diff --git a/.changeset/skill-family-better-near-auth.md b/.changeset/skill-family-better-near-auth.md new file mode 100644 index 000000000..a6eeb1718 --- /dev/null +++ b/.changeset/skill-family-better-near-auth.md @@ -0,0 +1,5 @@ +--- +"better-near-auth": minor +--- + +Upgrade all six skills with "the tasks you will actually be given" walkthroughs and grounded refusal-word → action tables (NEP-413 verify, nonce/replay, relay limits, sub-account rollback, session and wallet-connection failures), and extract the client skill's 50-line action tables into `references/client-actions.md`. diff --git a/.changeset/skill-family-content.md b/.changeset/skill-family-content.md new file mode 100644 index 000000000..5daa98473 --- /dev/null +++ b/.changeset/skill-family-content.md @@ -0,0 +1,5 @@ +--- +"everything-dev": minor +--- + +Add task-shaped skills `talk-to-the-app` (MCP/REST/RPC surfaces, API-key auth, discovery) and `add-a-route` (one complete contract → Effect handler → UI client → route → publish slice), and rework the public `/skill.md` into an entry-point router over the full 20-skill family with shared facts, pairings, and rules to work by. Every existing skill gains "the tasks you will actually be given" walkthroughs and grounded error-word → action tables. diff --git a/.changeset/skill-family-ui-surface.md b/.changeset/skill-family-ui-surface.md new file mode 100644 index 000000000..4f1381524 --- /dev/null +++ b/.changeset/skill-family-ui-surface.md @@ -0,0 +1,5 @@ +--- +"every-plugin": minor +--- + +Serve the platform skill family from the core UI build — `node_modules/{everything-dev,every-plugin,better-near-auth}/skills` copy into `dist/skills//` and are reachable at `/skills///SKILL.md` (with the rest of the family listed from `/skill.md`). Children get the same copies from the published npm tarballs. diff --git a/AGENTS.md b/AGENTS.md index b33a30af4..0f7ced8c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -187,6 +187,9 @@ tanstackIntent: - id: "every-plugin#plugin-testing" run: "pnpm dlx @tanstack/intent@latest load every-plugin#plugin-testing" for: "Test every-plugin modules with vitest and the plugin runtime. Use when writing or modifying plugin tests under plugins/*/src/__tests__/ or plugins/*/tests/." + - id: "everything-dev#add-a-route" + run: "pnpm dlx @tanstack/intent@latest load everything-dev#add-a-route" + for: "Add one API endpoint end to end in everything.dev — contract route with Zod schemas, Effect-native handler in the plugin router, UI call via useApiClient, route file rendering the data, typecheck, publish. Use when adding an API endpoint, wiring a new UI page to app data, or debugging the contract/handler/client chain." - id: "everything-dev#api-and-auth" run: "pnpm dlx @tanstack/intent@latest load everything-dev#api-and-auth" for: "API architecture, oRPC contracts, auth middleware, plugin-client composition, session handling, and client-side auth. Use when adding API routes, creating middleware, calling other plugins in-process, or integrating auth in routes and UI." @@ -217,6 +220,9 @@ tanstackIntent: - id: "everything-dev#super-app" run: "pnpm dlx @tanstack/intent@latest load everything-dev#super-app" for: "Build shared-host, shared-API super apps with tenant-specific UI composition. Use when setting up a base runtime plus custom tenant apps, configuring fixed-core multi-tenancy, reasoning about extends-based runtime lineage, or deciding what tenants can override today." + - id: "everything-dev#talk-to-the-app" + run: "pnpm dlx @tanstack/intent@latest load everything-dev#talk-to-the-app" + for: "Work a running everything.dev app over its API without cloning it — MCP tools, REST/OpenAPI, oRPC RPC, plugin RPC, API-key authentication, and discovery endpoints. Use when an agent needs to read or write app data (registry, proposals, votes, auth session, AI chat) over HTTP, choose between MCP/REST/RPC surfaces, or debug authentication and refused calls." - id: "everything-dev#ui-integration" run: "pnpm dlx @tanstack/intent@latest load everything-dev#ui-integration" for: "Route creation, API client usage, auth client, SSR hydration, sidebar system, and the @/app module surface. Use when adding new UI routes, fetching data from the API, implementing auth flows, or customizing sidebar navigation." diff --git a/packages/better-near-auth/skills/_artifacts/skill_tree.yaml b/packages/better-near-auth/skills/_artifacts/skill_tree.yaml index cf8b0187c..6d0bcd0f6 100644 --- a/packages/better-near-auth/skills/_artifacts/skill_tree.yaml +++ b/packages/better-near-auth/skills/_artifacts/skill_tree.yaml @@ -42,9 +42,12 @@ skills: domain: 'client' path: 'skills/client/SKILL.md' description: 'Set up the siwnClient plugin, configure wallet connection, use authClient.near actions for sign-in, account management, delegate action building, and relay submission' + references: + - 'references/client-actions.md' sources: - 'elliotBraem/better-near-auth:src/client.ts' - 'elliotBraem/better-near-auth:src/types.ts' + - 'elliotBraem/better-near-auth:src/index.ts' - 'elliotBraem/better-near-auth:README.md' - 'elliotBraem/better-near-auth:LLM.txt' - name: 'TanStack Router integration' @@ -103,3 +106,4 @@ skills: - 'elliotBraem/better-near-auth:examples/auth.everything.dev/api/src/index.ts' - 'elliotBraem/better-near-auth:examples/auth.everything.dev/api/src/lib/auth.ts' - 'elliotBraem/better-near-auth:examples/auth.everything.dev/api/src/lib/context.ts' + - 'elliotBraem/better-near-auth:src/index.ts' diff --git a/packages/better-near-auth/skills/auth-plugin/SKILL.md b/packages/better-near-auth/skills/auth-plugin/SKILL.md index 231bcbd70..b0c50cce9 100644 --- a/packages/better-near-auth/skills/auth-plugin/SKILL.md +++ b/packages/better-near-auth/skills/auth-plugin/SKILL.md @@ -36,6 +36,7 @@ sources: - "elliotBraem/better-near-auth:examples/auth.everything.dev/api/src/index.ts" - "elliotBraem/better-near-auth:examples/auth.everything.dev/api/src/lib/auth.ts" - "elliotBraem/better-near-auth:examples/auth.everything.dev/api/src/lib/context.ts" + - "elliotBraem/better-near-auth:src/index.ts" --- # Better-Near-Auth — Auth Plugin (everything.dev) @@ -348,3 +349,25 @@ interface AuthRequestContext { - **Cause**: Only scalar fields survive JSON round-trip (`bos.config.json`). Function-typed fields (`extendTx`, `onCreated`, `onRollback`, dynamic `init.args`) and binary data (`deploy.wasm`) cannot be expressed in a config file. - **Fix**: Use `bos.config.json` for `parentAccount`, `parentHasFullAccess`, `minDeposit`, `deploy.fromPublished`, static `init.args`. Configure `extendTx` / `onCreated` / `onRollback` / dynamic `init.args` / raw wasm through `siwn()` directly on the server instead of through the plugin. + +## The tasks you will actually be given + +**"Add NEAR sign-in to my everything-dev app and protect the settings pages."** +Register auth under `app.auth` in `bos.config.json` with `variables.siwn.recipients.{mainnet,testnet}`, keep `ui/src/lib/auth.ts` as the re-export of `everything-dev/ui/auth` (`createAuthClient`, `useAuthClient`, `sessionQueryOptions`), and add an `_authenticated.tsx` layout that runs `sessionQueryOptions(context.authClient, context.session)` in `beforeLoad` and redirects to `/login`. + +**"My plugin needs the signed-in user's NEAR account."** +In the plugin's `initialize` composed with auth, call `const auth = await plugins.auth({ context })` then `auth.getAuthContext()`; or in a route use the `requireAuth` middleware from `createAuthMiddleware(builder)` and read `context.userId` / `context.near.primaryAccountId` from the narrowed context. + +**"`authClient.near.client` no longer compiles."** +Removed in 1.8.1 — switch to `authClient.near.getNearClient()` (throws on the server, so call it only in client-side handlers). + +## What comes back when it refuses + +| What you see | Where it comes from | Action | +| --- | --- | --- | +| `Unauthorized: Invalid signature` (401) | Server `siwn()` recipient differs from the UI `siwnClient({ recipient })` | Stop — align `variables.siwn.recipient(s)` with the server config; retrying never helps | +| `getSession()` returns null on every SSR render | `createAuthClient({ runtimeConfig })` called without `headers: request.headers` | Stop — pass headers (and `cspNonce`) from `renderOptions` | +| Session cache stale after passkey/social sign-in | Query cached with `staleTime: 60s`; only NEAR flows auto-notify (1.8.2+) | Refresh once: `setQueryData(["session"], fresh)` then `invalidateQueries({ queryKey: ["session"] })` | +| `Sub-account creation unavailable on : parent key not configured...` (503) | `variables.siwn.subAccount` set but no `secrets.parentKey` server-side | Tell the user — the parent key is a server secret, not a config-file field | +| `This NEAR account is already linked to another user` (400) | NEAR account bound to a different user row | Stop — tell the user; do not retry | +| `Cannot unlink last authentication method. Link another account first.` (400) | Last auth method on the user | Tell the user to link another account first | diff --git a/packages/better-near-auth/skills/client/SKILL.md b/packages/better-near-auth/skills/client/SKILL.md index 5703b6e42..9bbf75613 100644 --- a/packages/better-near-auth/skills/client/SKILL.md +++ b/packages/better-near-auth/skills/client/SKILL.md @@ -18,6 +18,7 @@ sources: - "elliotBraem/better-near-auth:src/types.ts" - "elliotBraem/better-near-auth:README.md" - "elliotBraem/better-near-auth:LLM.txt" + - "elliotBraem/better-near-auth:src/index.ts" --- # Better-Near-Auth — Client Integration @@ -229,51 +230,7 @@ const result = await authClient.near.view({ ## Client Actions Reference -### authClient.near - -| Method | Returns | Description | -| ------ | ------- | ----------- | -| `nonce(params)` | `Promise>` | Request nonce from server | -| `verify(params)` | `Promise>` | Verify NEP-413 signature | -| `getProfile(accountId?)` | `Promise>` | Get NEAR profile | -| `view(params)` | `Promise>` | Server-side contract view call | -| `getAccountId()` | `string \| null` | The user's NEAR account ID. Prefers the live NearConnect connection; falls back to the primary SIWN-linked account on the session (`session.user.nearAccount`). Persists across disconnects. | -| `getState()` | `{ accountId, publicKey, networkId } \| null` | Wallet state | -| `isWalletConnected()` | `boolean` | Whether wallet is actively connected | -| `detectNearAccount()` | `Promise<{ accountId, publicKey, networkId } \| null>` | Silently probe for a previously authorized wallet without prompting | -| `ensureConnected()` | `Promise` | Reconnect wallet if disconnected | -| `disconnect()` | `Promise` | Disconnect wallet | -| `link(callbacks?)` | `Promise` | Link NEAR account to session | -| `unlink(params)` | `Promise` | Unlink NEAR account | -| `listAccounts()` | `Promise` | List linked NEAR accounts | -| `setPrimaryAccount(params)` | `Promise>` | Set primary linked NEAR account | -| `createSubAccount(params)` | `Promise>` | Create a sub-account | -| `checkSubAccountAvailability(params)` | `Promise>` | Check if a sub-account name is available | -| `buildSignedDelegateAction(receiverId, buildActions)` | `Promise` | Build + sign delegate action, returns base64 payload | -| `relayTransaction({ payload })` | `Promise>` | Submit delegate action to relayer | -| `getRelayStatus(txHash)` | `Promise>` | Check relayed tx status | -| `getRelayerInfo()` | `Promise>` | Get relayer info and balance | -| `relayHistory()` | `Promise>` | List relayed transactions | -| `setNetwork(network)` | `void` | Switch active network (mainnet/testnet) | -| `getNetwork()` | `"mainnet" \| "testnet"` | Get currently active network | -| `getSupportedNetworks()` | `("mainnet" \| "testnet")[]` | List supported networks | -| `getRecipient(network?)` | `string` | Get configured recipient for a network | -| `getNearClient()` | `Near` | Access near-kit Near instance (throws on server). Returns the `Near` client for direct transactions. | - -### authClient.signIn - -| Method | Description | -| ------ | ----------- | -| `near(callbacks?)` | Connect wallet, sign message, verify — single popup | - -### Callback Interface - -```typescript -interface AuthCallbacks { - onSuccess?: () => void; - onError?: (error: Error & { status?: number; code?: string }) => void; -} -``` +See [client-actions](references/client-actions.md) for the full `authClient.near.*` method table, `authClient.signIn.near`, and the `AuthCallbacks` interface (`onSuccess` / `onError`). ## Common Mistakes @@ -493,3 +450,32 @@ await authClient.near.createSubAccount({ Always check availability first to avoid unnecessary server round-trips and 409 CONFLICT errors. The availability check is cheap (regex + length check client-side, then RPC account lookup server-side). Source: src/client.ts:537-548, src/index.ts:1406-1412 + +## The tasks you will actually be given + +**"Add NEAR wallet sign-in and show the connected account in the header."** +Create one `authClient` with `siwnClient({ recipient })` (in this repo that factory lives in `packages/everything-dev/src/ui/auth.ts`, re-exported by `ui/src/lib/auth.ts`), call `authClient.signIn.near({ onSuccess, onError })`, and render `useNearAccountId(authClient)` from `better-near-auth/react` — never `getAccountId()` during render (not reactive). + +**"Let users write on-chain without paying gas."** +`const payload = await authClient.near.buildSignedDelegateAction(receiverId, builder)` → `await authClient.near.relayTransaction({ payload })` → poll `authClient.near.getRelayStatus(txHash)` until `status` is `"completed"` or `"failed"`. + +**"Create a per-user sub-account after sign-in."** +`checkSubAccountAvailability({ subAccountName })` first — the client returns `{ available: false, reason: "invalid" }` locally for bad names — then `createSubAccount({ subAccountName, publicKey })` with the user's key. + +**"The account ID disappears when the user disconnects the wallet."** +Read `useNearAccountId(authClient)` instead of `getState().accountId` — the getter falls back to the primary SIWN-linked session account (`session.user.nearAccount`) when NearConnect is disconnected or uninitialized. Use `isWalletConnected()` only to decide whether signing is possible. + +## What comes back when it refuses + +| What you see | Where it comes from | Action | +| --- | --- | --- | +| `Wallet not initialized for — this operation requires a browser environment` | Signing op called during SSR (`ensureConnected`, `buildSignedDelegateAction`, `signWithWallet`) | Stop — gate the call behind a client-only component/effect | +| `Wallet sign-in was cancelled or failed` | User closed the wallet popup | Tell the user; retry on demand, not automatically | +| `NEAR network changed while signing in` / `while connecting wallet` | Active network switched mid-flow | Retry once once the network is stable | +| `No NEAR account found — please sign in with your NEAR wallet` | Signing op with no wallet or session account | Tell the user to sign in | +| `Wallet connection required — please approve the connection to sign` | Wallet disconnected; `ensureConnected` prompt declined | Tell the user to approve the reconnect prompt | +| `Unauthorized: Invalid signature` (401, from verify) | Client recipient ≠ server `siwn()` recipient | Stop — fix `siwnClient({ recipient })`; the signature is valid, just for the wrong recipient | +| `Unauthorized: Nonce already used (replay attack detected)` (401) | Nonce replayed | Retry once with a fresh nonce from the `/near/nonce` endpoint | +| `No NEAR account linked to session` (401, relay/sub-account) | No SIWN-linked primary account | Tell the user to sign in | +| Availability `reason`: `taken`, `too-long`, `not-configured` | Server lookup in src/index.ts | Stop — pick another name (`not-configured` means fix server config) | +| 409 `Account already exists on ` | `createSubAccount` without availability check | Stop — check availability first | diff --git a/packages/better-near-auth/skills/client/references/client-actions.md b/packages/better-near-auth/skills/client/references/client-actions.md new file mode 100644 index 000000000..faa6a9ef3 --- /dev/null +++ b/packages/better-near-auth/skills/client/references/client-actions.md @@ -0,0 +1,49 @@ +# Client Actions Reference — authClient.near / authClient.signIn + +Full method tables for the `siwnClient()` Better Auth client plugin. See the `client` SKILL.md for setup, patterns, and common mistakes. + +## authClient.near + +| Method | Returns | Description | +| ------ | ------- | ----------- | +| `nonce(params)` | `Promise>` | Request nonce from server | +| `verify(params)` | `Promise>` | Verify NEP-413 signature | +| `getProfile(accountId?)` | `Promise>` | Get NEAR profile | +| `view(params)` | `Promise>` | Server-side contract view call | +| `getAccountId()` | `string \| null` | The user's NEAR account ID. Prefers the live NearConnect connection; falls back to the primary SIWN-linked account on the session (`session.user.nearAccount`). Persists across disconnects. | +| `getState()` | `{ accountId, publicKey, networkId } \| null` | Wallet state | +| `isWalletConnected()` | `boolean` | Whether wallet is actively connected | +| `detectNearAccount()` | `Promise<{ accountId, publicKey, networkId } \| null>` | Silently probe for a previously authorized wallet without prompting | +| `ensureConnected()` | `Promise` | Reconnect wallet if disconnected | +| `disconnect()` | `Promise` | Disconnect wallet | +| `link(callbacks?)` | `Promise` | Link NEAR account to session | +| `unlink(params)` | `Promise` | Unlink NEAR account | +| `listAccounts()` | `Promise` | List linked NEAR accounts | +| `setPrimaryAccount(params)` | `Promise>` | Set primary linked NEAR account | +| `createSubAccount(params)` | `Promise>` | Create a sub-account | +| `checkSubAccountAvailability(params)` | `Promise>` | Check if a sub-account name is available | +| `buildSignedDelegateAction(receiverId, buildActions)` | `Promise` | Build + sign delegate action, returns base64 payload | +| `relayTransaction({ payload })` | `Promise>` | Submit delegate action to relayer | +| `getRelayStatus(txHash)` | `Promise>` | Check relayed tx status | +| `getRelayerInfo()` | `Promise>` | Get relayer info and balance | +| `relayHistory()` | `Promise>` | List relayed transactions | +| `setNetwork(network)` | `void` | Switch active network (mainnet/testnet) | +| `getNetwork()` | `"mainnet" \| "testnet"` | Get currently active network | +| `getSupportedNetworks()` | `("mainnet" \| "testnet")[]` | List supported networks | +| `getRecipient(network?)` | `string` | Get configured recipient for a network | +| `getNearClient()` | `Near` | Access near-kit Near instance (throws on server). Returns the `Near` client for direct transactions. | + +## authClient.signIn + +| Method | Description | +| ------ | ----------- | +| `near(callbacks?)` | Connect wallet, sign message, verify — single popup | + +## Callback Interface + +```typescript +interface AuthCallbacks { + onSuccess?: () => void; + onError?: (error: Error & { status?: number; code?: string }) => void; +} +``` diff --git a/packages/better-near-auth/skills/relay/SKILL.md b/packages/better-near-auth/skills/relay/SKILL.md index 3bb162a46..ae77b4993 100644 --- a/packages/better-near-auth/skills/relay/SKILL.md +++ b/packages/better-near-auth/skills/relay/SKILL.md @@ -336,4 +336,27 @@ The ephemeral relayer encrypts its private key using HKDF-SHA256 derived from `B Source: src/utils.ts:21-41, src/index.ts:133 +## The tasks you will actually be given + +**"Set up gasless transactions for our dev server."** +Add `relayer: { whitelistedContracts: ["myapp.near"] }` to `siwn()` (omit `accountId` for ephemeral mode), restart, read the account ID from the `[siwn] Relayer initialized: ... (ephemeral)` startup log, and send NEAR to that implicit account — every relay fails until it is funded. + +**"Relay a write for the signed-in user."** +Build the payload with `authClient.near.buildSignedDelegateAction(receiverId, builder)` (client), then `POST /near/relay` via `relayTransaction({ payload })`; confirm with `getRelayStatus(txHash)`. + +**"A user's relay was submitted but never landed."** +Poll `GET /near/relay-status/:txHash` until `"completed"` or `"failed"`, and check `GET /near/relay-history` (rows live in the `relayedTransaction` table, written with `status: "pending"` at submit time). + +## What comes back when it refuses + +| What you see | Where it comes from | Action | +| --- | --- | --- | +| `Relayer not configured` (503) | `ensureRelayer` returned no state — config not passed or `BETTER_AUTH_SECRET` missing | Stop — check the `[siwn] Relayer initialized` startup log first | +| `Contract is not whitelisted for relay` (403) | `whitelistedContracts` check in src/index.ts | Stop — add the receiverId to `relayer.whitelistedContracts` or the user must pick another contract | +| `Transaction gas (...) exceeds relayer limit (...)` (400) | `maxGasPerTransaction` sum over all actions | Stop — lower the action gas or raise the limit | +| `Transaction deposit (...) exceeds relayer limit (...)` (400) | `maxDepositPerTransaction` sum over functionCall/transfer deposits | Stop — same as above for deposits | +| `Delegate action sender does not match session account` (401) | `senderId` ≠ primary linked account | Stop — rebuild the payload for the session's account; do not retry | +| 500 wrapping an RPC error (logged server-side, generic message on the wire) | `relayOnChain` threw — usually the unfunded ephemeral relayer | Tell the user after funding; do not blind-retry | +| `Relayer accountId "..." is set for but no privateKey was provided. Falling back to ephemeral mode.` (startup warning) | Explicit config missing `privateKey` | Stop — supply the key or drop `accountId` deliberately | + diff --git a/packages/better-near-auth/skills/siwn/SKILL.md b/packages/better-near-auth/skills/siwn/SKILL.md index d348c9df7..f83103ebf 100644 --- a/packages/better-near-auth/skills/siwn/SKILL.md +++ b/packages/better-near-auth/skills/siwn/SKILL.md @@ -397,3 +397,31 @@ The creation transaction must be signed by the parent account. If `parentAccount Source: src/index.ts:1403-1415, src/index.ts:1420-1423 See also: [subaccount skill](../subaccount/SKILL.md) + +## The tasks you will actually be given + +**"Add NEAR wallet sign-in to my Better Auth server."** +Add `siwn({ recipient, apiKey: process.env.FASTNEAR_API_KEY })` to `plugins` in the `betterAuth()` call (in this repo: `plugins/auth/src/auth-instance.ts`), run `npx @better-auth/cli generate` for the `nearAccount`/`relayedTransaction`/`relayerKey` tables, then wire the client with a matching `siwnClient({ recipient })`. + +**"Sign-in works on mainnet but fails for testnet accounts."** +Use `recipients: { mainnet, testnet }` instead of a single `recipient` — the plugin resolves per network (network is auto-detected from the account suffix: `.testnet` → testnet, otherwise mainnet). + +**"Show the user's NEAR profile (name/avatar)."** +Rely on the default FastNear KV → NEAR Social lookup, or pass `getProfile: async (accountId) => ...` to `siwn()` to return a `Profile` (or `null`) from your own source. + +**"Only certain function-call keys should be able to sign in."** +Pass `validateLimitedAccessKey: async ({ accountId, publicKey, recipient }) => boolean` — return `false` to reject the key with `Unauthorized: Invalid function call access key`. + +## What comes back when it refuses + +| What you see | Where it comes from | Action | +| --- | --- | --- | +| `Unauthorized: Invalid recipient` (401) | Signed recipient ≠ server `siwn()` recipient | Stop — align both sides; retrying cannot fix a config mismatch | +| `Unauthorized: Invalid signature` (401) | Signature fails NEP-413 verification | Retry the sign-in once (user may have signed a stale prompt); if it repeats, stop and check recipient/callbackUrl | +| `Invalid nonce` (400) | Nonce missing, not hex, or not 32 bytes | Retry once with a fresh nonce from `POST /near/nonce` | +| `Unauthorized: Nonce already used (replay attack detected)` (401) | Nonce replayed | Retry once with a fresh nonce — never reuse | +| `Network ID mismatch with account ID` (400) | `networkId` ≠ suffix-detected network (e.g. `.near` claimed as testnet) | Stop — pass the network matching the account suffix | +| `Unauthorized: Invalid function call access key` (401) | FAK not scoped to the recipient | Stop — or relax via `validateLimitedAccessKey` | +| `Nonce must be exactly 32 bytes` | Custom `getNonce` returns wrong length | Stop — fix the custom generator | +| `This NEAR account is already linked to another user` (400) | Account bound to a different user | Stop — tell the user | +| `Cannot unlink last authentication method. Link another account first.` (400) | Last auth method | Tell the user to link first | diff --git a/packages/better-near-auth/skills/subaccount/SKILL.md b/packages/better-near-auth/skills/subaccount/SKILL.md index 9572df88b..9b30bd3bd 100644 --- a/packages/better-near-auth/skills/subaccount/SKILL.md +++ b/packages/better-near-auth/skills/subaccount/SKILL.md @@ -454,3 +454,27 @@ subAccount: { parentKey: "ed25519:..." } secrets: { parentKey: "ed25519:..." } subAccount: { parentAccount: "myapp.near" } ``` + +## The tasks you will actually be given + +**"Give every signed-in user a sub-account under our parent."** +Configure `siwn({ subAccount: { parentAccount, parentHasFullAccess: true, minDeposit } })` (plus `relayer` or `secrets.parentKey`), then on the client run `checkSubAccountAvailability({ subAccountName })` → `createSubAccount({ subAccountName, publicKey })`. + +**"Deploy our contract into each new sub-account and initialize it."** +Add `deploy: { fromPublished: { accountId } }` and `init: { methodName: "init", args: (ctx) => ({ owner: ctx.userAccountId }) }` to `subAccount` — dynamic `init.args` requires the raw library, not `bos.config.json` (static args objects are fine there). + +**"onCreated writes rows to my own DB — make a failure clean up."** +Wrap your writes in `onCreated` and mirror cleanup in `onRollback` (delete only your own records — the plugin already deletes internal DB rows and runs `deleteAccount({ beneficiary: parentAccount })` on-chain). + +## What comes back when it refuses + +| What you see | Where it comes from | Action | +| --- | --- | --- | +| `Sub-account creation unavailable on : no parent account configured...` (503) | No `subAccount.parentAccount` and no named relayer | Stop — set `parentAccount` or use an explicit relayer | +| `Sub-account creation unavailable on : parent key not configured for ...` (503) | Parent set but no `secrets.parentKey` and no explicit relayer key | Stop — provide `secrets.parentKey` server-side | +| `Sub-account parent differs from relayer account. Provide secrets.parentKey to sign as the parent account.` (503) | Parent ≠ relayer, no parent key | Stop — server secret fix | +| `Account already exists on ` (409) | `createSubAccount` for an existing name | Stop — check `checkSubAccountAvailability` first | +| Availability `reason: "not-configured"` | No parent (or implicit hex parent) configured | Stop — fix server config | +| Availability `reason: "taken"` / `"too-long"` | On-chain lookup / 64-char limit (`.` length) | Stop — pick a shorter/new name | +| `Must have a linked NEAR wallet to check sub-account availability` (400) | No session-linked NEAR account | Tell the user to sign in with NEAR first | +| `Sub-account created but post-creation failed: ` (500) | `onCreated` threw — rollback already ran (DB rows deleted, on-chain account deleted) | Stop — fix the side effect that threw, then have the user retry creation | diff --git a/packages/better-near-auth/skills/tanstack/SKILL.md b/packages/better-near-auth/skills/tanstack/SKILL.md index 3411db905..da23cc96c 100644 --- a/packages/better-near-auth/skills/tanstack/SKILL.md +++ b/packages/better-near-auth/skills/tanstack/SKILL.md @@ -422,3 +422,30 @@ authClient: createAuthClient({ runtimeConfig: renderOptions.runtimeConfig }), `getHostUrl()`, `getAccount()`, and `getNetworkId()` read `window.__RUNTIME_CONFIG__` by default. On the server, `window` is undefined, so they throw. Always pass `{ runtimeConfig }` when calling `createAuthClient()` in `router.server.tsx` or `getRouteHead()`. Source: auth.ts:18-27 + +## The tasks you will actually be given + +**"Scaffold auth for a new SSR TanStack Router app."** +Create one `ui/src/lib/auth.ts` exporting `createAuthClient()` (with `siwnClient({ recipient, networkId })` and `credentials: "include"`), the `AuthClient` type, `useAuthClient()`, and `sessionQueryOptions` — then put `authClient: createAuthClient({ runtimeConfig })` in the router context of both `router.server.tsx` and `hydrate.tsx`. + +**"Wallet state is lost after navigation."** +Something is calling a factory per render/call, creating fresh `nearState`/`walletConnected` atoms. Make `createAuthClient` a create-once router-context singleton and read it with `useAuthClient()`; never call it inside components. + +**"Sign-in works, but authenticated routes bounce to /login on SSR."** +The server-side client can't see cookies: pass `runtimeConfig` in `router.server.tsx` and forward request headers (`credentials: "include"` alone is not enough for SSR), then guard with `ensureQueryData(sessionQueryOptions(...))` in `beforeLoad`. + +**"Direct writes fail with the wallet asking to reconnect."** +Call `await authClient.near.ensureConnected()` before `getNearClient()...send()` — `buildSignedDelegateAction` does this for you; direct `.send()` does not. + +## What comes back when it refuses + +| What you see | Where it comes from | Action | +| --- | --- | --- | +| `Wallet not initialized for — this operation requires a browser environment` | `ensureConnected` / `buildSignedDelegateAction` / `signIn.near` called during SSR | Stop — gate the call behind a client-only component or effect | +| `No NEAR account found — please sign in with your NEAR wallet` | Signing op before any wallet or session account | Tell the user to sign in | +| `Wallet connection required — please approve the connection to sign` | `ensureConnected` prompt declined | Tell the user to approve the wallet prompt | +| `NEAR network changed while signing in` | Network switched mid-sign-in | Retry once after the network settles | +| `Wallet sign-in was cancelled or failed` | Popup closed by the user | Tell the user; retry on demand, not automatically | +| `Session gas keys are not enabled on this deployment` | `addSessionGasKey()` on a server without the gas-key config | Stop — server-side config, not a client fix | +| Redirect loop to `/login` on SSR | Missing `headers`/`runtimeConfig` in the server `createAuthClient` call | Stop — fix the `router.server.tsx` context wiring | +| Wallet state (accountId) vanishes after client navigation | Multiple `siwnClient()` instances, each with its own atom | Stop — collapse to one router-context client | diff --git a/packages/every-plugin/skills/plugin-client/SKILL.md b/packages/every-plugin/skills/plugin-client/SKILL.md index 654c1c57e..0fb8d8237 100644 --- a/packages/every-plugin/skills/plugin-client/SKILL.md +++ b/packages/every-plugin/skills/plugin-client/SKILL.md @@ -447,3 +447,23 @@ See the `extends-config` skill for deep merge semantics, per-environment extends - **Not saving the API key secret** — `authClient.apiKey.create()` returns the full key string (`edk_...`) only once. Subsequent calls to `list` or `update` return only the prefix and metadata. - **Plugin key vs plugin name confusion** — the client namespace is the `bos.config.json` `plugins` key (e.g., `"apps"`), not the Module Federation `name` field. They often match but are not guaranteed to. - **Forgetting to regenerate types** — after adding/removing plugins in `bos.config.json`, run `bos types gen` or restart `bos dev`. Stale `api-types.gen.ts` will have missing or wrong namespace keys. + +## The tasks you will actually be given + +**"Call a plugin route from a script"** — create a key in the UI under `/settings/api-keys` (the full `edk_...` secret is shown once — copy it), then use the external `RPCLink` + `x-api-key` snippet from the External / Standalone API Client section above. The procedure path is `POST {hostUrl}/api/rpc/{pluginKey}/{procedure}` — e.g. `client.registry.listRegistryApps({ limit: 24 })`. + +**"Wire a typed API call into a UI route"** — import `useApiClient` from `@/app`, call `apiClient..()`. If the namespace key is missing in the types, the plugin was added/removed without regenerating: run `bos types gen` (or restart `pnpm run dev`), never hand-edit `ui/src/lib/api-types.gen.ts`. + +**"Set up a typed client for another language"** — pull the spec from `GET {hostUrl}/api/spec.json` and run the openapi codegen commands in the OpenAPI / REST section above. + +## What comes back when it refuses + +| Error word / shape you see | Meaning | Action | +|---|---|---| +| 401 `UNAUTHORIZED` "Authentication required" with `data: { hint: "Sign in or provide an API key" }` | no session cookie and no API key reached the middleware | sign in, or attach `x-api-key: edk_...` | +| 401 `UNAUTHORIZED` "API key required" with `data: { authType: "apiKey", hint: "Provide a valid API key via x-api-key header" }` | the route is `requireApiKey(...)`-protected and your key was absent — usually `Authorization: Bearer` was used instead | switch to the `x-api-key` header | +| 403 `FORBIDDEN` "API key lacks permission: :" with `data: { requiredPermissions, keyPermissions }` | the key's `permissions` map does not cover every required resource/action — partial matches are rejected | re-create (server-side) or update the key with the needed permissions | +| 403 `FORBIDDEN` "Requires role: ..." / "Requires organization role: ..." | session role gate | sign in with an account holding the role; a key cannot satisfy these | +| 403 `FORBIDDEN` "Organization requires platform-admin approval" | the org's `status` is not `"active"` | stop — needs platform-admin approval, no client-side fix | +| `authClient.apiKey.list` / `update` return only `prefix` and metadata | expected — the full secret is returned once at `create` | create a new key; the old secret is unrecoverable | +| Browser request blocked by CORS despite a valid key | session cookies not sent (`credentials: "include"` missing) or origin not allowed by `CORS_ORIGIN` | add `credentials: "include"` to the fetch wrapper, or fix `CORS_ORIGIN` | diff --git a/packages/every-plugin/skills/plugin-development/SKILL.md b/packages/every-plugin/skills/plugin-development/SKILL.md index e6ca3fd1c..da20d56cb 100644 --- a/packages/every-plugin/skills/plugin-development/SKILL.md +++ b/packages/every-plugin/skills/plugin-development/SKILL.md @@ -2,7 +2,7 @@ name: plugin-development description: Build every-plugin modules with oRPC contracts, Effect services, and Module Federation. Use when creating or modifying plugins under plugins/ or the _template scaffold. metadata: - sources: "src/plugin.ts,src/errors.ts,src/types.ts" + sources: "src/plugin.ts,src/errors.ts,src/types.ts,src/runtime/errors.ts,src/runtime/services/module-federation.service.ts,src/runtime/index.ts,plugins/_template/src/index.ts" --- # every-plugin Development @@ -273,3 +273,22 @@ export default { - Using `Effect.runPromise` inside `Effect.gen` — use `yield*` instead for proper error channel - Putting business logic in `createRouter` — keep it in the service class, router is just glue - Using `Effect.provide(Tag, Layer.scoped(...))` inside `initialize` for long-lived resources — creates a transient scope that releases the resource immediately after initialization. Use `buildScoped(Tag, Layer.scoped(...))` instead + +## The tasks you will actually be given + +**"Add a route to a plugin"** — e.g. a `deleteItem` procedure. Add it to `plugins//src/contract.ts` (`.route({ method: "DELETE", path: "/items/{id}" }).input(...).errors(Errors)`), add a method on the service class, then wire it in `src/index.ts` as `deleteItem: builder.deleteItem.effect(function* ({ input, context, errors }) { ... })` — check `context.userId` first and fail with `errors.UNAUTHORIZED(...)` / `errors.NOT_FOUND(...)` inside the generator, mirroring `plugins/_template/src/index.ts` (`getById`, `deleteThing`). Verify with `cd plugins/ && pnpm test`, then `pnpm run typecheck` (regenerates `plugins-types.gen.ts` for cross-plugin consumers). + +**"Ship a new plugin"** — copy `plugins/_template/` to `plugins//`, set `name` in its package.json (this is the registry id tests use), fill in `plugin.dev.ts` (port; plugins start at 3010), and register it in the authored config with `Plugin("").path("plugins/", { ... })` (see `bos.app.ts`). Run `bos types gen` or restart `pnpm run dev`, then deploy with `cd plugins/ && bos plugin publish `. + +**"A plugin fails to load in the host"** — capture the error words and match them in the table below; shared-identity failures are confirmed with `bos mf check` and fixed by rebuilding/redeploying the one plugin. + +## What comes back when it fails + +| Error word / shape you see | Meaning | Action | +|---|---|---| +| `ModuleFederationError` with "shared identity mismatch" (or the plugin's `mf-manifest.json` reports an older `pluginVersion` than the host) | plugin bundle built against a different `@module-federation/runtime`, or missing a `shared[]` dep the host requires | rebuild + redeploy that one plugin (`cd plugins/ && bos plugin publish `); confirm with `bos mf check`. Stop editing code — this is a build/deploy problem | +| `No valid plugin constructor found for ''` / `missing the required 'binding' property` | the remote module's export was not created by `createPlugin()` | `export default createPlugin({...})` from `src/index.ts`, rebuild | +| `PluginRuntimeError` with operation `validate-config` or `validate-secrets` and a Zod cause | runtime config violates the plugin's `variables`/`secrets` schemas (secrets hydrate into variables, then re-validate) | fix `plugin.dev.ts` in dev, or the runtime config entry; read the Zod issue paths in the error | +| `Plugin ID '' not found in registry.` | registry key mismatch | align the key used with the plugins/ directory key and package.json `name` | +| 401 `UNAUTHORIZED` with data `{ apiKeyProvided: boolean, ... }` | no session or API key reached the handler (see `every-plugin/errors`) | caller-side problem — sign in or send `x-api-key`; do not catch it in the plugin | +| 503 `SERVICE_UNAVAILABLE` / 502 `CONNECTION_ERROR` shapes from `every-plugin/errors` | upstream dependency of the service failed | surface them via `.errors(...)` on the contract, not ad-hoc ORPCError codes | diff --git a/packages/every-plugin/skills/plugin-testing/SKILL.md b/packages/every-plugin/skills/plugin-testing/SKILL.md index 400ea283d..0205ba522 100644 --- a/packages/every-plugin/skills/plugin-testing/SKILL.md +++ b/packages/every-plugin/skills/plugin-testing/SKILL.md @@ -2,7 +2,7 @@ name: plugin-testing description: Test every-plugin modules with vitest and the plugin runtime. Use when writing or modifying plugin tests under plugins/*/src/__tests__/ or plugins/*/tests/. metadata: - sources: "src/index.ts,src/runtime/index.ts" + sources: "src/index.ts,src/runtime/index.ts,src/runtime/errors.ts,plugins/_template/tests/setup.ts" --- # every-plugin Testing @@ -225,3 +225,22 @@ export default defineConfig({ - Forgetting `vite-tsconfig-paths` plugin — package subpath imports like `every-plugin/errors` won't resolve - Omitting the `registry` object when calling `createPluginRuntime(...)` — the runtime requires explicit plugin entries - Testing only happy paths — always test error channels (Effect failures, ORPCError throws) + +## The tasks you will actually be given + +**"Add an integration test for a new route"** — in `plugins//tests/integration/plugin.test.ts`, follow the `_template` pattern (`plugins/_template/tests/setup.ts`): call `await getPluginClient({ userId: "user123" })` — it boots `createPluginRuntime` with the registry keyed by the package.json `name`, config from `plugin.dev.ts`, and an HTTP server that maps `x-test-user`/`x-test-session` headers to context. Then drive the route through the client and assert the response. Run with `cd plugins/ && pnpm test` (script `vitest run`, `testTimeout: 30000` in `vitest.config.ts`). + +**"Prove a service method fails with the right ORPC code"** — follow `plugins/_template/tests/unit/things-service.test.ts`: build a fresh layer with `DatabaseLive('pglite:')` and `Layer.succeed(PluginIdTag, "")`, run the failing effect through `Effect.runPromiseExit` + `Cause.squash`, then `expect(error).toBeInstanceOf(ORPCError)` and `expect(error.code).toBe("CONFLICT")` (or `NOT_FOUND`). Clean the temp dir in `afterEach`. + +**"A test fails at `usePlugin` with a validation error"** — the config comes from `plugin.dev.ts` via `tests/setup.ts` (`TEST_CONFIG`). The runtime validates `variables` and `secrets` against the plugin's zod schemas (`validate-config` / `validate-secrets` → `PluginRuntimeError` with a `zodError` cause). Fix the config in `plugin.dev.ts`, not the test. + +## What comes back when it fails + +| Error word / shape you see | Meaning | Action | +|---|---|---| +| `Plugin ID '' not found in registry.` (`PluginRuntimeError`, operation `validate-plugin-id`) | the `usePlugin` key does not match a registry entry | make the registry key the package.json `name` (as `tests/setup.ts` does), then read again | +| `PluginRuntimeError` with operation `validate-config` / `validate-secrets` and a Zod cause | `TEST_CONFIG` violates the plugin's schemas | fix `plugin.dev.ts`; do not loosen the schemas to make tests pass | +| `ModuleFederationError` | only when loading a remote URL — in-process tests (`module: Plugin`) never produce it | if you see it, your registry entry points at a URL instead of the imported module | +| `Cannot find module` for `every-plugin/...` subpaths | `vite-tsconfig-paths` missing from `vitest.config.ts` plugins | add it (see the Vitest Config section) | +| Test hangs past 30s | default `testTimeout: 30000`; a streaming handler's `for await` never terminates | pass `signal`/`maxResults` limits like `_template`'s `listenBackground`, or abort the iterator | +| Scoped resource still alive after tests finish | `runtime.shutdown()` (or `_template`'s `teardown()`) never ran | call it in `afterAll` | diff --git a/packages/every-plugin/src/build/ui/factory.ts b/packages/every-plugin/src/build/ui/factory.ts index 44f701be5..f7acdb6c9 100644 --- a/packages/every-plugin/src/build/ui/factory.ts +++ b/packages/every-plugin/src/build/ui/factory.ts @@ -12,6 +12,33 @@ import path from "node:path"; import { CORE_UI_PLUGIN_KEY } from "../../ui/manifest/contract"; import { createUiRsbuildConfig } from "./rsbuild-config"; +/** + * The platform skill family ships inside the framework packages' npm tarballs + * (`files: ["skills"]`), so resolving them from `node_modules` gives the core + * ui the same copies in the parent workspace (pnpm symlinks) and every + * generated child repo (published tarball). Served at `/skills//`. + */ +const SKILL_PACKAGES = ["everything-dev", "every-plugin", "better-near-auth"] as const; + +export function platformSkillCopies(workspaceRoot: string): Array<{ + from: string; + to: string; + globOptions: { ignore: string[] }; +}> { + return SKILL_PACKAGES.flatMap((name) => { + const from = path.join(workspaceRoot, "node_modules", name, "skills"); + return fs.existsSync(from) + ? [ + { + from, + to: `./skills/${name}`, + globOptions: { ignore: ["**/_artifacts/**"] }, + }, + ] + : []; + }); +} + export interface CoreUiRsbuildConfigInput { /** Authored config domain — stamped as `import.meta.env.APP_NAME`. */ domain?: string; @@ -57,7 +84,10 @@ export function createCoreUiRsbuildConfig({ domain, account }: CoreUiRsbuildConf webExposes: CORE_UI_WEB_EXPOSES, nodeEntry: CORE_UI_NODE_ENTRY, nodeExposes: CORE_UI_NODE_EXPOSES, - copy: [{ from: path.join(workspaceRoot, "public"), to: "./" }], + copy: [ + { from: path.join(workspaceRoot, "public"), to: "./" }, + ...platformSkillCopies(workspaceRoot), + ], define: { "import.meta.env.APP_NAME": JSON.stringify(domain), "import.meta.env.APP_ACCOUNT": JSON.stringify(account), diff --git a/packages/every-plugin/src/build/ui/rsbuild-config.ts b/packages/every-plugin/src/build/ui/rsbuild-config.ts index 7805e045a..17f97c30c 100644 --- a/packages/every-plugin/src/build/ui/rsbuild-config.ts +++ b/packages/every-plugin/src/build/ui/rsbuild-config.ts @@ -38,7 +38,11 @@ export interface UiRsbuildConfigOptions { webExposes: Record; nodeEntry: string; nodeExposes: Record; - copy?: Array<{ from: string; to: string }>; + copy?: Array<{ + from: string; + to: string; + globOptions?: { ignore?: string[] }; + }>; define?: Record; /** routes dir relative to the rsbuild cwd — folder-form ui sources live at * `ui/src/routes` while the build runs from the plugin root */ diff --git a/packages/everything-dev/skills/add-a-route/SKILL.md b/packages/everything-dev/skills/add-a-route/SKILL.md new file mode 100644 index 000000000..2aad65ab3 --- /dev/null +++ b/packages/everything-dev/skills/add-a-route/SKILL.md @@ -0,0 +1,132 @@ +--- +name: add-a-route +description: Add one API endpoint end to end in everything.dev — contract route with Zod schemas, Effect-native handler in the plugin router, UI call via useApiClient, route file rendering the data, typecheck, publish. Use when adding an API endpoint, wiring a new UI page to app data, or debugging the contract/handler/client chain. +metadata: + sources: "api/src/contract.ts,api/src/index.ts,plugins/_template/src/contract.ts,plugins/_template/src/index.ts,packages/every-plugin/src/errors.ts,ui/src/routes,packages/everything-dev/skills/ui-integration/SKILL.md" +--- + +# Add a route + +One endpoint, end to end. The slice below is complete — copy it, rename, and +it runs. Everything is typed from the contract: the handler's `input`/`context`, +the generated client, and the UI's response shape. + +## Where the route lives + +| The task in front of you | Put it in | +|---|---| +| App-shell glue (health, infra) | `api/src/contract.ts` + `api/src/index.ts` | +| A feature (things, registry, proposals, votes, chat) | a plugin under `plugins//` | + +Feature routes live in plugins; `api/` is a slim shell. New plugins: copy +`plugins/_template/` (`everything-dev#plugin-development`). + +## The slice + +### 1. Contract (`plugins//src/contract.ts`) + +```ts +import { oc } from "@orpc/contract"; +import { z } from "zod"; +import { FORBIDDEN, NOT_FOUND, UNAUTHORIZED } from "every-plugin/errors"; + +export const WidgetSchema = z.object({ + id: z.string(), + title: z.string(), +}); + +export const contract = oc.router({ + getWidget: oc + .route({ method: "GET", path: "/widgets/{id}" }) + .input(z.object({ id: z.string().min(1) })) + .output(z.object({ widget: WidgetSchema })) + .errors({ NOT_FOUND, UNAUTHORIZED, FORBIDDEN }), +}); + +export type ContractType = typeof contract; +``` + +Path params (`{id}`) bind to the Zod input keys automatically. + +### 2. Handler (`plugins//src/index.ts`) + +```ts +export default createPlugin.withPlugins()({ + // variables, secrets, context, contract — see the template + createRouter: (builder) => ({ + getWidget: builder.getWidget.effect(function* ({ input, context, errors }) { + if (!context.userId) { + return yield* Effect.fail(errors.UNAUTHORIZED({ apiKeyProvided: !!context.apiKey })); + } + const widgets = yield* WidgetsService; + const widget = yield* widgets.getWidget(input.id); + return { widget }; + }), + }), +}); +``` + +The tag (`WidgetsService`) must be exposed from the returned `initialize` +layer. Handlers are Effect generators — see **Rules** below. + +### 3. UI call (any component) + +```ts +import { useApiClient } from "@/app"; + +const apiClient = useApiClient(); +const { data } = await apiClient.template.listThings({ limit: 20 }); +``` + +The client is namespaced by the plugin key in `bos.app.ts` — a `widgets` +plugin is `apiClient.widgets.*` — and typed from the contract; +`ui/src/lib/api-types.gen.ts` regenerates on `pnpm run typecheck` / `bos dev`. + +### 4. Route file (`ui/src/routes/.tsx`) + +File-based routing — TanStack Router generates the tree. See +`everything-dev#ui-integration` for loaders, SSR, and the page-header/testid +conventions. + +### 5. Verify, then publish + +```bash +pnpm run typecheck # regenerates types, then checks everything +pnpm run build # the train — never per-workspace raw builds +pnpm run deploy # preflight → build → upload → publish → image +``` + +## The tasks you will actually be given + +**"Add a read-only endpoint for X."** Contract → handler → UI query. No auth +schema needed; still add `.errors({ NOT_FOUND })` for typed misses. + +**"Add an endpoint only signed-in users can write."** Same slice, plus the +`context.userId` guard from step 2. For roles, orgs, or API-key permissions, +load `everything-dev#api-and-auth` for the middleware pattern. + +**"The UI can't see the new route."** Types are generated: run +`pnpm run typecheck` (self-sufficient), or restart `bos dev`. If the client +name is wrong, the plugin key in `bos.app.ts` is the namespace. + +## What comes back when it fails + +| word | do | +|---|---| +| `Type 'DecoratedMiddleware'…` in `.use()` | middleware typing does not compose with `.use()` — use the local-middleware pattern (`plugins/proposals/src`) | +| `UNAUTHORIZED` type error | its `data` requires `{ apiKeyProvided: boolean }`; all-optional shapes still need explicit `data: {}` | +| Effect lint flags floating effects | `pnpm run lint:effect`; `tsc` clean is not enough | +| `ModuleFederationError` / `__webpack_modules__` | the deployed plugin's `mf-manifest.json` is older than the host's — `bos mf check`, then republish the plugin | +| 404 on the new route | the route lives in a plugin — the path is `/api/rpc//…` or `/api/` | + +## Rules to work by + +- **`.effect(function* ...)`, never new `.handler(async)`.** Services come + from `yield* Tag` in generators; `Context.get(context["effect/context"], Tag)` + is reserved for streaming (async-generator) handlers. +- **`Effect.fail`, not throws.** Definitive failure exits are + `return yield* Effect.fail(...)`; shared shapes come from `every-plugin/errors`. +- **No `Effect.provide(Tag, Layer.effect(...))` for persistent deps.** Build + scoped services with `buildScoped(...)` inside `initialize` — transient + scopes release immediately. +- **Typecheck before publish.** `pnpm run typecheck` regenerates all types. diff --git a/packages/everything-dev/skills/api-and-auth/SKILL.md b/packages/everything-dev/skills/api-and-auth/SKILL.md index cacace763..42768b5b5 100644 --- a/packages/everything-dev/skills/api-and-auth/SKILL.md +++ b/packages/everything-dev/skills/api-and-auth/SKILL.md @@ -2,7 +2,7 @@ name: api-and-auth description: API architecture, oRPC contracts, auth middleware, plugin-client composition, session handling, and client-side auth. Use when adding API routes, creating middleware, calling other plugins in-process, or integrating auth in routes and UI. metadata: - sources: "api/src/index.ts,api/src/contract.ts,packages/everything-dev/src/api/auth-middleware.ts,host/src/services/auth.ts,host/src/services/plugins.ts,host/src/program.ts,ui/src/lib/auth.ts,ui/src/lib/api.ts" + sources: "api/src/index.ts,api/src/contract.ts,packages/everything-dev/src/api/auth-middleware.ts,packages/every-plugin/src/errors.ts,host/src/services/auth.ts,host/src/services/plugins.ts,host/src/program.ts,ui/src/lib/auth.ts,ui/src/lib/api.ts" --- # API Architecture & Auth @@ -423,3 +423,21 @@ for await (const event of iterator) { ## How Routes Are Mounted The host (`host/src/program.ts`) creates RPC and OpenAPI handlers from each plugin's router, mounted at `/api/rpc/`. The session middleware runs on `/api/*` before the RPC handlers, ensuring context is set. + +## The tasks you will actually be given + +**"Add an admin-only route."** Contract declares `.errors({ UNAUTHORIZED, FORBIDDEN })`; handler is `.effect(function* ...)` and guards with `return yield* Effect.fail(errors.UNAUTHORIZED({ apiKeyProvided: !!context.apiKey }))` — note all-optional error shapes still need an explicit `data: {}`. For shared middleware shapes, see `packages/everything-dev/src/api/auth-middleware.ts`. + +**"Call the auth plugin from my route."** `getAuthClient(services, { reqHeaders: context.reqHeaders })` then `await authClient.getSession()` — in-process, no HTTP roundtrip. + +**"Wire an SSE route."** Contract: `.output(eventIterator(Schema))`. Handler: async generator that subscribes to the publisher and yields per event, filtering server-side on `pluginId`/query params. + +## What comes back when it fails + +| word | do | +|---|---| +| `Type 'DecoratedMiddleware'…` on `.use()` | middleware typing does not compose with the `.use()` builder — use the local-middleware pattern (`plugins/proposals/src`) | +| `UNAUTHORIZED` type error on the fail call | its `data` requires `{ apiKeyProvided: boolean }`; all-optional error shapes still need explicit `data: {}` | +| runtime 401 with what looks like a valid session | the session middleware resolution returned nulls — check `GET /api/auth/getSession` before blaming the route | +| `tsc` clean but Effect diagnostics | run `pnpm run lint:effect` (oxlint type-aware) — floating effects and tag mismatches surface there | +| `ModuleFederationError` / `N plugin(s) failed to load` at startup | the API plugin itself failed to load — `bos mf check`, republish the lagging plugin, restart the host | diff --git a/packages/everything-dev/skills/cli-reference/SKILL.md b/packages/everything-dev/skills/cli-reference/SKILL.md index 78f8595b5..6d2f187e1 100644 --- a/packages/everything-dev/skills/cli-reference/SKILL.md +++ b/packages/everything-dev/skills/cli-reference/SKILL.md @@ -267,3 +267,11 @@ bos db studio my-plugin # open for a custom plugin ``` Requires the `DATABASE_URL` secret to be set on the target plugin. Uses the plugin's drizzle.config.ts for schema discovery. + +## The tasks you will actually be given + +**"Regenerate the types."** `bos types gen` — or just `pnpm run typecheck`, which regenerates first and then checks. Never hand-edit `*.gen.ts` files. + +**"Which command deploys?"** `pnpm run deploy` (the full train: preflight → build → upload → publish → image). Config-only is `bos publish`; one plugin is `bos plugin publish `. + +**"What is running right now?"** `bos ps` for tracked dev processes, `bos status` for health and versions, `ls .bos/logs/` for per-service logs. diff --git a/packages/everything-dev/skills/code-style/SKILL.md b/packages/everything-dev/skills/code-style/SKILL.md index 88ababf37..083ce27a5 100644 --- a/packages/everything-dev/skills/code-style/SKILL.md +++ b/packages/everything-dev/skills/code-style/SKILL.md @@ -60,3 +60,11 @@ metadata: - If neighboring files use `function Component()`, don't use `const Component: React.FC = () =>` - If neighboring files group imports by type (React, third-party, local), do the same - Consistent file structure within a directory is more important than personal preference + +## The tasks you will actually be given + +**"Review my new component for style."** Check four things: file is kebab-case with a PascalCase named export, classes use semantic tokens (`bg-background`, `text-muted-foreground`) not hardcoded colors, no inline comments, imports use `@/` aliases grouped like the neighbors. + +**"Where does this shared component go?"** `ui/src/components/ui/.tsx`, named export, added to `ui/src/components/index.ts`. Feature-specific components stay colocated with the route that uses them. + +**"Is this file name ok?"** Lowercase kebab-case for every file and directory, including routes. PascalCase file names are the only hard violation; route path segments follow TanStack conventions (`_` prefix for layouts). diff --git a/packages/everything-dev/skills/dev-workflow/SKILL.md b/packages/everything-dev/skills/dev-workflow/SKILL.md index 8bc4297a0..f32753f0f 100644 --- a/packages/everything-dev/skills/dev-workflow/SKILL.md +++ b/packages/everything-dev/skills/dev-workflow/SKILL.md @@ -2,7 +2,7 @@ name: dev-workflow description: Development workflow for everything-dev projects using bos dev, bos start, and the Module Federation runtime. Use when starting dev servers, debugging hot reload, or understanding the service-descriptor architecture. metadata: - sources: "packages/everything-dev/src/service-descriptor.ts,packages/everything-dev/src/orchestrator.ts,packages/everything-dev/src/dev-logs.ts,packages/everything-dev/src/dev-session.ts,packages/everything-dev/src/process-registry.ts,packages/everything-dev/src/app.ts" + sources: "packages/everything-dev/src/service-descriptor.ts,packages/everything-dev/src/orchestrator.ts,packages/everything-dev/src/dev-logs.ts,packages/everything-dev/src/dev-session.ts,packages/everything-dev/src/process-registry.ts,packages/everything-dev/src/app.ts,packages/everything-dev/src/infra/preflight.ts,packages/everything-dev/src/dev-program.ts" --- > **Config form:** the authored config is `bos.app.ts` (canonical, preferred when both exist). A legacy `bos.config.json` is still supported for older children. Where this doc says `bos.config.json` for the *local authored file*, read "the authored config". The published artifact on FastKV keeps the key name `bos.config.json`. @@ -198,3 +198,21 @@ planned `bos dev --workspaces` orchestrator), `ports`, optional `budget`, `start orchestrator owns registry entries. Process tracking uses `~/.cache/everything-dev/pids.json` (global, atomic writes). + +## The tasks you will actually be given + +**"Start dev on my own ports."** `bos dev --port 3100` (api 3101, auth 3102, ui 3103, plugins 3110+ derive from the base). Explicit `--*-port` flags are pinned and persisted; derived ones are not. + +**"My change didn't show up."** Match the change type to the reload path: UI and API hot-reload instantly; auth/plugins/config changes need `bos kill && bos dev`; stale generated types need `pnpm run typecheck`. + +**"API is down."** `bos ps` → read `.bos/logs/api.log` → `curl http://localhost:3001/remoteEntry.js` — in that order. + +## What comes back when it fails + +| word | do | +|---|---| +| `Infra preflight failed: …` | the DB preflight probes localhost ports and auto-runs `docker compose up -d --wait` when every failure is a down local service — if it still fails, start Postgres (`pnpm run dev:postgres`) and read the failure messages | +| `PortAllocationError` on an explicit port | the pinned port is occupied and allocation fails loudly — free it or pick another; it never silently moves an explicit choice | +| `+100` port-shift notice at startup | expected drift: the preferred block was busy, holders are named in the notice, nothing is persisted — restarts re-try the preferred base | +| service stuck in `starting` | probe its readiness path directly (e.g. `curl http://localhost:3003/remoteEntry.js`) and read `.bos/logs/.log` | +| stale registry entries blocking restart | `bos ps` / `bos kill` prune dead PIDs on read; to wipe manually: `rm ~/.cache/everything-dev/pids.json` | diff --git a/packages/everything-dev/skills/extends-config/SKILL.md b/packages/everything-dev/skills/extends-config/SKILL.md index 1c4466860..061b982b3 100644 --- a/packages/everything-dev/skills/extends-config/SKILL.md +++ b/packages/everything-dev/skills/extends-config/SKILL.md @@ -188,3 +188,20 @@ From `packages/everything-dev/src/merge.ts`: - `resolveExtendsRef(extendsField, env)` — resolve string|object extends for a given env - `rebuildOrderedConfig(config)` — enforce canonical ordering - `BOS_CONFIG_ORDER` — ordered field names + +## The tasks you will actually be given + +**"Remove a plugin I inherited."** Set it to `null` in the authored config's `plugins` — the null sentinel deletes it from the merged result. Deleting the entry entirely just inherits the parent's. + +**"Different parent for staging."** Object-form `extends` keyed by env (`development` / `staging` / `production`); the fallback chain is requested env → `production` → first defined value. + +**"Why doesn't the runtime see my config edit?"** Dev reads `.bos/bos.resolved-config.json` (regenerated on every `bos dev`); production reads FastKV. The authored config is only the publish input — `bos publish` is the write. + +## What comes back when it refuses + +| word | do | +|---|---| +| `CircularExtendsError` — "Circular extends detected: A -> B -> A" | stop — the extends chain loops; restructure so no runtime extends itself transitively | +| `Circular dependency detected among: … dependsOn` | build-order cycle in the build train — fix the `dependsOn` declarations in the authored config | +| inherited plugin still present after "removing" it | deep merge keeps parent plugins unless nulled — you need the explicit `null` sentinel, keyed exactly as the parent's plugin key | +| your section is missing from `.bos/bos.resolved-config.json` | ordering never drops keys — if it's absent, the authored config or its parent didn't define it; check the `extends` chain with `bos config --full` | diff --git a/packages/everything-dev/skills/init-upgrade/SKILL.md b/packages/everything-dev/skills/init-upgrade/SKILL.md index 3ca7846cb..a063a3c2f 100644 --- a/packages/everything-dev/skills/init-upgrade/SKILL.md +++ b/packages/everything-dev/skills/init-upgrade/SKILL.md @@ -2,7 +2,7 @@ name: init-upgrade description: bos init, bos sync, and bos upgrade workflows — template download, snapshot-based conflict detection, package version bumps, and how init/sync select and own files. Use when scaffolding new projects, syncing upstream changes, or upgrading framework packages. metadata: - sources: "packages/everything-dev/src/cli/init.ts,packages/everything-dev/src/cli/sync.ts,packages/everything-dev/src/cli/upgrade.ts,packages/everything-dev/src/cli/snapshot.ts,packages/everything-dev/src/merge.ts" + sources: "packages/everything-dev/src/cli/init.ts,packages/everything-dev/src/cli/sync.ts,packages/everything-dev/src/cli/upgrade.ts,packages/everything-dev/src/cli/snapshot.ts,packages/everything-dev/src/cli.ts,packages/everything-dev/src/merge.ts" --- > **Config form:** the authored config is `bos.app.ts` (canonical, preferred when both exist). A legacy `bos.config.json` is still supported for older children. Where this doc says `bos.config.json` for the *local authored file*, read "the authored config". The published artifact on FastKV keeps the key name `bos.config.json`. @@ -199,3 +199,20 @@ All writes to the authored config enforce `BOS_CONFIG_ORDER`: `extends` → `account` → `domain` → `title` → `description` → `testnet` → `staging` → `repository` → `ci` → `app` → `plugins` Unknown keys go after known keys. See `everything-dev#extends-config` for full ordering details. + +## The tasks you will actually be given + +**"Scaffold a child that extends the base runtime."** `bos init --extends bos:/// --account .near --domain --no-interactive`, then customize `account` / `domain` / `app.ui` in the authored config; init's tail runs `pnpm install` + `bos types gen` (unless `--no-install`). + +**"Sync reports conflicted files."** Your local edits diverged from the snapshot; upgrade applies the template version and leaves your copy in `.bos/sync-backup/` — diff and merge back, or `bos sync --force` deliberately. + +**"Upgrade framework packages."** `bos upgrade --dry-run` first, then `bos upgrade` — it bumps the root catalog, rewrites workspace refs to `catalog:`, syncs the template, and rewrites legacy imports. + +## What comes back when it refuses + +| word | do | +|---|---| +| file listed as `conflicted` in sync results | local file modified since the snapshot AND the template changed — take the template version (your copy is backed up at `.bos/sync-backup/`) or re-apply your change on top | +| sync keeps skipping my file | app-owned files that diverged from the snapshot are skipped without `--force`; framework-owned files always update when the template changes | +| `Circular extends detected while resolving upgrade source` | the upgrade source's extends chain loops — resolve from a non-cyclic parent | +| init copies fewer files than expected | `buildInitPatterns` filters to the selected plugins + their routes; pass `--overrides ui,api,host` to widen the scaffold | diff --git a/packages/everything-dev/skills/plugin-development/SKILL.md b/packages/everything-dev/skills/plugin-development/SKILL.md index 51a065592..92b810f49 100644 --- a/packages/everything-dev/skills/plugin-development/SKILL.md +++ b/packages/everything-dev/skills/plugin-development/SKILL.md @@ -2,7 +2,7 @@ name: plugin-development description: Build, register, and deploy plugins within everything.dev. Covers the _template scaffold, contract/service/index pattern, database setup with Drizzle, authored-config registration, plugin UI/sidebar, and CLI workflow. Use when creating new plugins, adding database-backed routes, or deploying plugins to production. metadata: - sources: "plugins/_template/src/index.ts,plugins/_template/src/contract.ts,plugins/_template/src/service.ts,packages/every-plugin/src/build/rspack,plugins/_template/plugin.dev.ts,plugins/_template/src/db/schema.ts,plugins/_template/src/db/layer.ts,api/src/db/index.ts,api/src/db/migrate.ts,packages/every-plugin/src/plugin.ts" + sources: "plugins/_template/src/index.ts,plugins/_template/src/contract.ts,plugins/_template/src/service.ts,packages/every-plugin/src/build/rspack,plugins/_template/plugin.dev.ts,plugins/_template/src/db/schema.ts,plugins/_template/src/db/layer.ts,api/src/db/index.ts,api/src/db/migrate.ts,packages/every-plugin/src/plugin.ts,packages/everything-dev/src/cli.ts" --- > **Config form:** the authored config is `bos.app.ts` (canonical, preferred when both exist). A legacy `bos.config.json` is still supported for older children. Where this doc says `bos.config.json` for the *local authored file*, read "the authored config". The published artifact on FastKV keeps the key name `bos.config.json`. @@ -395,3 +395,21 @@ Builds the plugin, pins the deterministic image-native production URL into the p 3. `bos types gen` 4. `bos dev` to develop 5. `bos plugin publish` to deploy + +## The tasks you will actually be given + +**"Add a DB-backed route."** Schema in `src/db/schema.ts` → `drizzle-kit generate` → inside `initialize`, build the repo with `buildScoped(RepoTag, RepoLive.pipe(Layer.provide(DatabaseLive(config.secrets.YOURPLUGIN_DATABASE_URL))))` → handler is `.effect(function* ...)` and does `yield* RepoTag`. + +**"Register and deploy a new plugin."** `bos plugin add local:plugins/your-plugin` (writes the authored config + regenerates types) → `pnpm run typecheck` → `bos plugin publish your-plugin` → restart the host. + +**"Expose a service to the host, not just oRPC."** Set `servicesTag` on the plugin definition — handlers keep accessing it with `yield* Tag` as normal. + +## What comes back when it fails + +| word | do | +|---|---| +| pool dead / connection released right after startup | a transient scope — you extracted the driver instead of building the service via `buildScoped(...)` inside `initialize`; rebuild it that way (see [database](references/database.md)) | +| `ModuleFederationError` / `__webpack_modules__[e].call` in the browser | the deployed `mf-manifest.json` is older than the host's federation runtime — `bos mf check`, then `bos plugin publish `, restart the host | +| migration never runs | migrations execute inside `DatabaseLive`'s scoped layer at plugin boot — confirm the plugin booted and the `_DATABASE_URL` secret resolves | +| handler can't see `context.userId` | the context zod schema is a filter — declare every field you use or the host's value is stripped | +| stale types in `plugins-client.gen.ts` | `bos types gen` (or `pnpm run typecheck`) after editing any contract | diff --git a/packages/everything-dev/skills/publish-sync/SKILL.md b/packages/everything-dev/skills/publish-sync/SKILL.md index 0c0a40933..08e889274 100644 --- a/packages/everything-dev/skills/publish-sync/SKILL.md +++ b/packages/everything-dev/skills/publish-sync/SKILL.md @@ -244,3 +244,22 @@ bos kill # Kill all tracked processes pnpm install # Reinstall deps bos dev # Restart ``` + +## The tasks you will actually be given + +**"Ship one plugin only."** `bos plugin publish ` (builds + pins the deterministic URL + publishes config), then `bos mf check` from the repo root to confirm federation compatibility. + +**"Roll back a bad publish."** `bos rollback --previous` — republishes a verified FastKV snapshot without touching git; the manual path is `git checkout -- bos.app.ts` + `bos publish`. + +**"Deploy my own runtime under my own account."** NEAR account + `bos key generate` → `NEAR_PRIVATE_KEY`; authored config sets your `account` + `extends` the parent and keeps `domain` as the parent gateway; `bos publish --deploy`; run the image with `BOS_ACCOUNT` / `BOS_GATEWAY`. + +## What comes back when it refuses + +| word | do | +|---|---| +| publish returns `status: "error"` before signing | preflight failed — key resolution (no `NEAR_PRIVATE_KEY` / credentials / keychain) or the registry read; run `bos publish --dry-run` to see the plan | +| config resolves to nothing at `bos:///` | namespace = signer — the tx was signed by a different account than the config's `account`; sign with the publisher's key (see the `registry` skill) | +| `integrity mismatch for ` | the remote entry's SRI hash diverges from the published config — redeploy that workspace and republish; the host blocks HTML/SSR on mismatch and client-renders without the remote | +| `N plugin(s) failed to load` at host startup | `bos mf check` — a plugin's `metaData.pluginVersion` lags the host; republish that plugin | +| `missing-local-path` error on `--packages local` | a selected entry has no `development: local:` (remote-only) — it can't be rebuilt from this repo; drop it from the selection | +| publish prints nothing / skips | FastKV already holds an identical config — a free no-op, nothing to fix | diff --git a/packages/everything-dev/skills/registry/SKILL.md b/packages/everything-dev/skills/registry/SKILL.md index 93ec741e0..e3f965f56 100644 --- a/packages/everything-dev/skills/registry/SKILL.md +++ b/packages/everything-dev/skills/registry/SKILL.md @@ -63,3 +63,20 @@ bos registry use v1.citynode.near/citynode.app --sections app.ui,plugins.apps - **Config "missing" after publishing with the wrong key** — the write landed under the signing account's namespace, not the config's `account`. Sign with the account named in the authored config. - **Editing the committed authored config URLs and expecting runtime changes** — the runtime source of truth is FastKV; the repo copy is the publish *input*. - **Expecting near-cli-rs for publish** — signing is in-process; near-cli-rs is only needed for `bos key generate` (interactive keychain) and account management. + +## The tasks you will actually be given + +**"Attach a section from another runtime."** `bos registry use / --sections app.ui,plugins.apps --dry-run` first, then the real run, then `bos types gen` to refresh generated types. + +**"Debug: my publish isn't visible."** Fetch the exact read URL (`https://kv.main.fastnear.com/v0/latest/dev.everything.near//apps%2F%2F%2Fbos.config.json`) and compare the response's `predecessor_id` to the account you signed with. + +**"Publish without double-spending gas."** Just run `bos publish` — `isConfigAlreadyPublished` skips identical configs before signing, and `waitForPublishedConfig` confirms the read-back (~120s timeout). + +## What comes back when it refuses + +| word | do | +|---|---| +| read 404 / config "missing" right after publish | the write landed under the signer's namespace, not the config's `account` — sign with the account named in the authored config (`bos key generate` FCAK, or the wallet/delegate) | +| publish errors before signing | preflight failed — key resolution or registry URL; check `NEAR_PRIVATE_KEY` / `BOS_NEAR_PRIVATE_KEY` / `~/.near-credentials//.json` | +| `waitForPublishedConfig` timeout | the read-back didn't match within ~120s — fetch the URL by hand; if `predecessor_id` is wrong, the wrong key signed | +| relayer-signed publish invisible | a relayer signing its own tx writes to the *relayer's* namespace — there is no sign-on-behalf; use the publisher's FCAK or a NEP-366 delegate | diff --git a/packages/everything-dev/skills/super-app/SKILL.md b/packages/everything-dev/skills/super-app/SKILL.md index 1dcbf5f24..80d9abca2 100644 --- a/packages/everything-dev/skills/super-app/SKILL.md +++ b/packages/everything-dev/skills/super-app/SKILL.md @@ -2,7 +2,7 @@ name: super-app description: Build shared-host, shared-API super apps with tenant-specific UI composition. Use when setting up a base runtime plus custom tenant apps, configuring fixed-core multi-tenancy, reasoning about extends-based runtime lineage, or deciding what tenants can override today. metadata: - sources: "host/src/services/tenant-runtime.ts,host/src/program.ts,host/src/services/federation.server.ts,packages/everything-dev/src/config.ts" + sources: "host/src/services/tenant-runtime.ts,host/src/services/binding-resolver.ts,host/src/program.ts,host/src/services/federation.server.ts,packages/everything-dev/src/config.ts" --- # Super Apps @@ -168,3 +168,20 @@ After publishing config changes that affect the base host runtime, restart the h - Use `everything-dev#extends-config` for deep-merge and resolved-config semantics. - Use `everything-dev#publish-sync` for publish and deploy steps. - Use this `super-app` skill when the question is specifically about the shared-host, shared-API multi-tenant architecture. + +## The tasks you will actually be given + +**"Set up a shared-host tenant."** Base: `bos init --overrides ui,api,host` then `bos publish --deploy`. Child: authored config with `extends` + its own `account`/`domain`, override `app.ui` only, publish it; register the tenant row with the right allow flags in the DB. + +**"Tenant overrides are not applying."** Walk the checklist in order: config exists in FastKV → extends the base runtime → tenant record allow flags set → host logs (`cat .bos/logs/host.log`) → integrity hashes on the tenant remotes valid. + +**"This tenant needs SSR."** Set `allow_ssr` on the tenant record — the host's BindingResolver reads permissions from `GET /tenants/bindings` (cached 30s); without the flag the tenant falls back to client rendering. + +## What comes back when it refuses + +| word | do | +|---|---| +| tenant config missing in FastKV | the host silently serves the base runtime — no error surfaces to the browser; verify the published path and the signer namespace | +| `Tenant resolution is unavailable: the API plugin failed to load` | the BindingResolver can't reach `/api/tenants/bindings` — check `GET /api/_health` and the startup plugin-load errors, republish the API | +| integrity rejection of a tenant remote | HTML/SSR requests block immediately, assets fall back — redeploy the tenant bundle and republish so the SRI matches | +| overrides still stale after publishing | the host boots one base snapshot and caches tenant configs — restart the host (or wait out the cache TTL) | diff --git a/packages/everything-dev/skills/talk-to-the-app/SKILL.md b/packages/everything-dev/skills/talk-to-the-app/SKILL.md new file mode 100644 index 000000000..cfec63cfa --- /dev/null +++ b/packages/everything-dev/skills/talk-to-the-app/SKILL.md @@ -0,0 +1,138 @@ +--- +name: talk-to-the-app +description: Work a running everything.dev app over its API without cloning it — MCP tools, REST/OpenAPI, oRPC RPC, plugin RPC, API-key authentication, and discovery endpoints. Use when an agent needs to read or write app data (registry, proposals, votes, auth session, AI chat) over HTTP, choose between MCP/REST/RPC surfaces, or debug authentication and refused calls. +metadata: + sources: "host/src/routes/api.ts,host/src/services/mcp.ts,ui/public/skill.md,plugins/registry/src/contract.ts,plugins/proposals/src/contract.ts,plugins/votes/src/contract.ts,plugins/ai/src/contract.ts,api/src/contract.ts" +--- + +# Talk to the app + +You act against a **running everything.dev instance** — no clone, no build. +Every operation in the app's API is reachable four ways; they differ only in +transport. The data is identical. + +## Read this page, then choose a surface + +| The task in front of you | Use | +|---|---| +| An MCP client (Claude, Cursor, …) drives the whole API | [MCP](#mcp) | +| A script or curl one-off — GET/POST against documented paths | [REST](#rest-openapi) | +| A typed JSON-RPC call by procedure name | [oRPC RPC](#orpc-rpc) | +| Anything else: rate limits, auth failures, which surface failed | this file | + +**Load the skill for the task in front of you; do not load them all.** + +## Facts every call relies on + +| | local dev | production | +|---|---|---| +| API base | `http://localhost:3001` | the app's origin (`/api/*` is same-origin) | +| REST path | `/api/` | `/api/` | +| oRPC RPC | `/api/rpc/...` | `/api/rpc/...` | +| MCP | `POST /api/mcp` | `POST /api/mcp` | +| Auth header | `x-api-key: api_...` | same | + +There is no network parameter anywhere — the origin is the choice. + +## Authentication + +All API operations require authentication. Use an **API key**: + +1. Sign in with your NEAR wallet (SIWN) at the app. +2. Navigate to **Settings → API Keys** at `/settings/api-keys`. +3. Create a new key — the full secret (`api_...`) is shown once, copy it immediately. +4. Pass it on every request: + +``` +x-api-key: api_your_key_here +``` + +The header works for `/api/*` (REST), `/api/rpc/*` (oRPC RPC), and `/api/mcp` +(MCP). Keys carry optional permissions; a call outside them refuses with 403. + +## MCP + +Streamable HTTP, stateless — no session ID required: + +``` +POST /api/mcp +``` + +Connect your MCP client to `/api/mcp`. Tools are auto-generated from +the API's OpenAPI spec, so every API operation — including auth, relay, and +API-key management — appears as a typed tool. Discovery: + +``` +GET /.well-known/mcp.json +``` + +Returns the server name, endpoint URL, and auth scheme. + +## REST / OpenAPI + +- **API docs UI**: `GET /api` (Scalar reference) +- **OpenAPI spec**: `GET /api/spec.json` +- REST paths are the contract paths under `/api`: + +| operation | path | +|---|---| +| health | `GET /api/ping` | +| published registry apps | `GET /api/v1/registry/apps` | +| one registry app | `GET /api/v1/registry/apps/{accountId}/{gatewayId}` | +| registry status | `GET /api/v1/registry/status` | +| proposals lifecycle | `POST /api/v1/proposals`, `.../approve`, `.../reject`, `.../reopen` | +| upvotes | `POST /api/v1/upvotes`, `GET /api/v1/upvotes/{entityId}/count` | +| AI chat | `POST /api/v1/chat` (streaming) | + +## oRPC RPC + +Typed JSON-RPC by procedure name, one mount per plugin: + +``` +POST /api/rpc/{plugin}/{procedure} +Content-Type: application/json + +["input-or-array"] +``` + +- `POST /api/rpc/ping` — the API shell (mounted at the root) +- `POST /api/rpc/registry/listRegistryApps` — registry plugin +- `POST /api/rpc/auth/getSession` — auth plugin (special-cased mount) +- AI chat streaming lives at `POST /api/rpc/ai/chat` + +## The tasks you will actually be given + +**"What can this app do?"** Read `GET /api/spec.json` — it is the full typed +surface, and the MCP tools are generated from exactly it. + +**"List the published apps."** `GET /api/v1/registry/apps` (or the +`listRegistryApps` MCP tool). Paginate with `limit`. + +**"Check my identity / session."** `POST /api/rpc/auth/getSession` with the key. +An empty session means the key's owner has no active browser session — keys +identify the user, they do not create sessions. + +**"Vote on / read the voting feed."** Counts and reads via +`/api/v1/upvotes/*`; casting needs the session of a signed-in user or a key +with write permission. + +## Rules to work by + +- **Read before writing.** `GET /api/spec.json` first; every write route names + its auth requirement in the spec. +- **A key is not a session.** `x-api-key` identifies an owner; sign-in flows + (`better-near-auth#client`) create sessions. Use the key for agent work. +- **One failed call is information.** See below before retrying. + +## What comes back when it refuses + +| status | do | +|---|---| +| `401` | no or bad key — re-read `/settings/api-keys`; check the `x-api-key` header reached the request | +| `403` | the key lacks permission — the owner edits the key's permissions; do not retry unchanged | +| `404` | wrong surface or path — re-check `/api/spec.json`; plugin RPC mounts are `/api/rpc/` | +| `429` | rate limited — wait as the response says, then once more | +| `5xx` | repeat later; on a write it may have been made — read first to confirm | + +A refused write changes nothing: verify by reading, then repeat only if +nothing is there. diff --git a/packages/everything-dev/skills/ui-integration/SKILL.md b/packages/everything-dev/skills/ui-integration/SKILL.md index 50a75351e..fac98db75 100644 --- a/packages/everything-dev/skills/ui-integration/SKILL.md +++ b/packages/everything-dev/skills/ui-integration/SKILL.md @@ -409,3 +409,21 @@ function Component() { - Shared UI components: `ui/src/components/ui/` — semantic, reusable primitives - Feature components: colocated with the route that uses them - Exports from `ui/src/components/index.ts` for shared components + +## The tasks you will actually be given + +**"Add a page that lists data."** File under `ui/src/routes/` (nest under `_layout/_authenticated/` to inherit the guard) → `loader` calls `apiClient..(...)` from context → component reads `Route.useLoaderData()` → add the sidebar entry in `ui/src/components/layout/nav-items.ts`. + +**"Add an auth-only page."** Nest under `_layout/_authenticated/` — the guard's `beforeLoad` already redirects to `/login?redirect=`; return `{ auth: … }` from `beforeLoad` to type the context downstream. + +**"Call the API in a component."** `const apiClient = useApiClient()` for one-shot calls; `const orpc = useOrpc()` when you want `useQuery`/`useMutation` caching. + +## What comes back when it fails + +| word | do | +|---|---| +| `ORPCError` with `code: "UNAUTHORIZED"` in the browser | the session middleware resolved nulls — check `authClient.getSession()`; if the guard should have caught it, verify the route actually nests under `_layout/_authenticated/` | +| `Route.` or `apiClient.` has no types | generated types are stale — `pnpm run typecheck` (regenerates `api-types.gen.ts`) or restart `bos dev` | +| `TimeoutError` on `page.waitForURL` after a `` click (browser tests) | client-side nav never fires `load` — pass `{ waitUntil: "commit" }` | +| client-only value crashes SSR | wrap it: `useClientValue(() => getAppName(), "app")` from `@/hooks` | +| edits to `*.gen.*` files keep vanishing | they are generated stubs — change the source (`router.tsx`, `app.ts`, the contracts) instead | diff --git a/tests/regression/browser/helpers/page-ready.ts b/tests/regression/browser/helpers/page-ready.ts index 49fae5c3b..e58a9ba21 100644 --- a/tests/regression/browser/helpers/page-ready.ts +++ b/tests/regression/browser/helpers/page-ready.ts @@ -31,10 +31,10 @@ export function collectErrors(page: Page): PageErrors { export async function waitForApp(page: Page): Promise { await page.waitForSelector("#root", { timeout: 30000 }); - const hasRuntimeConfig = await page.evaluate(() => { - return typeof window.__RUNTIME_CONFIG__ !== "undefined"; - }); - expect(hasRuntimeConfig).toBeTruthy(); + await page.waitForFunction( + () => typeof (window as { __RUNTIME_CONFIG__?: unknown }).__RUNTIME_CONFIG__ !== "undefined", + { timeout: 30000 }, + ); try { await page.waitForFunction( diff --git a/tests/regression/http/agent_surface_test.go b/tests/regression/http/agent_surface_test.go index 94c6dbafa..834c9be7b 100644 --- a/tests/regression/http/agent_surface_test.go +++ b/tests/regression/http/agent_surface_test.go @@ -24,12 +24,26 @@ func TestAgentSurface(t *testing.T) { regtest.MustContain(t, body, "/api/mcp") regtest.MustContain(t, body, "x-api-key") regtest.MustContain(t, body, "/settings/api-keys") + }) - ct := "" - for _, line := range strings.Split(body, "\n") { - _ = line + t.Run("skill_family_served", func(t *testing.T) { + for _, path := range []string{ + "/skills/everything-dev/add-a-route/SKILL.md", + "/skills/everything-dev/talk-to-the-app/SKILL.md", + "/skills/every-plugin/plugin-development/SKILL.md", + "/skills/better-near-auth/client/SKILL.md", + } { + status, _, body := regtest.GetRaw(t, client, baseURL+path) + regtest.MustStatus(t, status, 200, body) + regtest.MustContain(t, body, "name:") } - _ = ct + }) + + t.Run("skill_md_mentions_router_loads_one_skill", func(t *testing.T) { + status, _, body := regtest.GetRaw(t, client, baseURL+"/skill.md") + regtest.MustStatus(t, status, 200, body) + regtest.MustContain(t, body, "/skills/everything-dev/talk-to-the-app/SKILL.md") + regtest.MustContain(t, body, "load ONE skill") }) t.Run("well_known_mcp_json", func(t *testing.T) { diff --git a/ui/public/llms.txt b/ui/public/llms.txt index 3ea3ed257..fa2538901 100644 --- a/ui/public/llms.txt +++ b/ui/public/llms.txt @@ -4,7 +4,15 @@ ## Skills -- [Skill](/skill.md): Agent-ready prompt for talking to, running, editing, and publishing this runtime. +- [Skill](/skill.md): Entry-point router — pick the one skill for the task in front of you. Load ONE skill, not all. +- [Talk to the app](/skills/everything-dev/talk-to-the-app/SKILL.md): MCP, REST, oRPC RPC, API keys, discovery — work the running app without cloning it. +- [Add a route](/skills/everything-dev/add-a-route/SKILL.md): one complete API endpoint end to end — contract, router, UI route, typecheck, publish. +- [UI integration](/skills/everything-dev/ui-integration/SKILL.md): file-based routing, API client, auth client, SSR hydration, sidebar. +- [Plugin development](/skills/everything-dev/plugin-development/SKILL.md): build, register, and deploy plugins in this repo. +- [API and auth](/skills/everything-dev/api-and-auth/SKILL.md): auth middleware, plugin-client composition, session handling. +- [Auth plugin](/skills/better-near-auth/auth-plugin/SKILL.md): mount and consume better-near-auth; SIWN, API keys, organizations, passkeys. +- [every-plugin skills](/skills/every-plugin/plugin-development/SKILL.md): the oRPC/Effect/Module Federation machinery under every plugin. +- All 20 skills: `/skills/{everything-dev,every-plugin,better-near-auth}//SKILL.md` — also loadable with `npx @tanstack/intent@latest load #`. ## API diff --git a/ui/public/skill.md b/ui/public/skill.md index d911ee2f4..b582ad2b8 100644 --- a/ui/public/skill.md +++ b/ui/public/skill.md @@ -7,198 +7,85 @@ There are two ways to work with this app: 1. **Talk to the app** — use the API via MCP or REST to read/write data without cloning anything. 2. **Clone and modify** — clone the repository, run locally, edit code, and publish. -## Mode 1: Talk to the app +## Read this page, then load ONE skill -### MCP endpoint +Each skill below is self-contained: its `SKILL.md` covers the common case and routes to its own `references/` for the rest. **Load the skill for the task in front of you; do not load them all.** -The API is exposed as an MCP (Model Context Protocol) server at: - -``` -POST /api/mcp -``` - -Transport: Streamable HTTP (stateless, no session ID required). - -Connect your MCP client to `{origin}/api/mcp` and it will discover all available tools automatically. Tools are generated from the API's OpenAPI spec — each API operation becomes a tool with typed input parameters. - -### Authentication - -All API operations require authentication. Use an **API key**: - -1. Sign in with your NEAR wallet at the website (Sign-In-With-NEAR / SIWN). -2. Navigate to **Settings → API Keys** at `/settings/api-keys`. -3. Create a new key. The full secret (`api_...`) is shown once — copy it immediately. -4. Pass the key on every request: - -``` -x-api-key: api_your_key_here -``` - -This header works for `/api/*` (REST), `/api/rpc/*` (oRPC RPC), and `/api/mcp` (MCP). - -### REST / OpenAPI - -- **API docs UI**: `GET /api` (Scalar reference) -- **OpenAPI spec**: `GET /api/spec.json` -- **oRPC RPC**: `POST /api/rpc/{procedure}` (typed JSON-RPC) -- **Plugin RPC**: `POST /api/rpc/{plugin}/{procedure}` (e.g. `/api/rpc/auth/getSession`) - -### MCP discovery - -``` -GET /.well-known/mcp.json -``` - -Returns a JSON descriptor with the server name, endpoint URL, and auth scheme. - -### Available operations - -The API shell exposes health and error-surface helpers; the feature surface lives in plugins: - -- **API shell**: `ping` (health), `errors` (regression error kinds) -- **Auth plugin** (`/api/rpc/auth/*`): session, NEAR SIWN, relay, gas keys, API keys, organizations, passkeys -- **Template plugin** (`/api/rpc/template/*`): generic typed things store (create, get, list, delete, SSE stream) -- **Registry plugin** (`/api/rpc/registry/*`): published runtime registry + app metadata -- **Proposals plugin** (`/api/rpc/proposals/*`): proposal lifecycle -- **Votes plugin** (`/api/rpc/votes/*`): voting feed -- **AI plugin** (`/api/rpc/ai/*`): OpenAI-compatible chat streaming - -## Mode 2: Clone and modify - -### TanStack Intent - -- Registry entry: `https://tanstack.com/intent/registry/everything-dev` -- Load with TanStack Intent: `npx @tanstack/intent@latest load everything-dev` -- If the agent supports registry URLs directly, point it at the registry entry above. - -### What this repo is - -This repo is the **everything.dev base runtime** — the source of truth for the published framework packages (`every-plugin`, `better-near-auth`, `everything-dev`), the universal runtime image, and the everything.dev app itself (host, slim UI/API shells, plugins). It is not a generated child project. - -- Work across `host/`, `api/`, `ui/`, `plugins/`, and `packages/` as needed. -- The host is kept generic. UI is meant to be combined and put together. The API is a slim plugin shell; feature routes live in plugins. -- **Remotes are not hosted APIs** — they are code bundles loaded via Module Federation at runtime. Everything runs in this runtime process, not on a remote server. -- It is okay to update the runtime and runtime-owned code. This repository is the main runtime, actively being improved and simplified. -- A generated child repo created by `bos init` works primarily in `ui/src/` and its authored config, inheriting the upstream host, auth, and API. - -### Read AGENTS.md first - -After cloning, read **`AGENTS.md`** at the repo root. It contains: - -- The TanStack Intent skills block (loadable skills for everything-dev, every-plugin, better-near-auth) -- Operational guidance: dev workflow, architecture, code changes, plugin architecture, testing, security -- Links to Matt Pocock workflow skills in `.agents/skills/` (TDD, code review, bug diagnosis, planning) -- Regression test instructions - -### Simplified route structure - -Public routes (no auth): - -- `/` — landing: what the runtime is, how it works, link to the repository -- `/about` — renders the repo `README.md` via a README-fetching loader -- `/skill` — renders this `skill.md` -- `/skill.md`, `/llms.txt` — raw doc endpoints - -Authed routes (behind `_authenticated`): - -- `/login` — NEAR wallet sign-in entry (SIWN) -- `/dashboard` — authenticated landing (next steps, identity) -- `/orgs` — Better-Auth organizations (members, teams, invitations, API keys) -- `/things`, `/things/$thingId`, `/things/new`, `/things/live` — generic typed table demo (durable store + SSE via the template plugin) -- `/settings/*` — user settings (profile, auth methods, security, API keys) — grafted from the auth plugin UI -- `/chat` — AI plugin chat (grafted from the AI plugin UI) -- `/admin/*` — admin surface: overview + system (admin role only) - -Keep `_layout` and `_authenticated` generic. Do not bake app-specific product concepts into the scaffold shell. - -### Scaffold a new everything.dev app - -Use `bos init` for new child apps. - -```bash -bos init your-app.everything.dev \ - --extends dev.everything.near/everything.dev \ - --account your-account.near \ - --overrides ui \ - --no-interactive -``` - -If your installed `bos` version rejects `--no-interactive` or other expected init flags, use one of these fallbacks: - -```bash -bunx everything-dev@latest init your-app.everything.dev -``` - -or run `bos init` interactively and answer the prompts. - -### Run locally +Every skill is at `{origin}/skills///SKILL.md` (the files it points to are relative to that URL), and loads with TanStack Intent from the npm package of the same name: ```bash -cp .env.example .env -bun install -docker compose up -d --wait # Start local Postgres -bos dev +npx @tanstack/intent@latest load # ``` -Useful variants: +Registry entry: `https://tanstack.com/intent/registry/everything-dev` (and `every-plugin`, `better-near-auth`). + +| The task in front of you | Load | +|---|---| +| Talk to the running app without cloning it — MCP, REST, oRPC RPC, API keys, discovery | [`everything-dev#talk-to-the-app`](/skills/everything-dev/talk-to-the-app/SKILL.md) | +| Add an API endpoint end to end — contract, router, UI route, typecheck, publish | [`everything-dev#add-a-route`](/skills/everything-dev/add-a-route/SKILL.md) | +| Add or edit a UI route — file-based routing, API client, auth client, SSR, sidebar | [`everything-dev#ui-integration`](/skills/everything-dev/ui-integration/SKILL.md) | +| Build a new plugin in this repo — scaffold, contract/service/index, database, registration | [`everything-dev#plugin-development`](/skills/everything-dev/plugin-development/SKILL.md) | +| Test plugins with vitest and the plugin runtime | [`every-plugin#plugin-testing`](/skills/every-plugin/plugin-testing/SKILL.md) | +| Consume deployed plugins from an external app, child project, or script | [`every-plugin#plugin-client`](/skills/every-plugin/plugin-client/SKILL.md) | +| Understand the every-plugin machinery under a plugin — oRPC contracts, Effect services, Module Federation | [`every-plugin#plugin-development`](/skills/every-plugin/plugin-development/SKILL.md) | +| Add API routes, auth middleware, plugin-client composition, session handling | [`everything-dev#api-and-auth`](/skills/everything-dev/api-and-auth/SKILL.md) | +| Wire NEAR wallet sign-in into the UI — authClient.near actions, delegate actions | [`better-near-auth#client`](/skills/better-near-auth/client/SKILL.md) | +| Set up or debug the SIWN server plugin — NEP-413, nonces, verification | [`better-near-auth#siwn`](/skills/better-near-auth/siwn/SKILL.md) | +| Configure the gasless delegate-action relayer or debug relay failures | [`better-near-auth#relay`](/skills/better-near-auth/relay/SKILL.md) | +| Configure sub-account creation, contract deployment, lifecycle rollback | [`better-near-auth#subaccount`](/skills/better-near-auth/subaccount/SKILL.md) | +| Mount and consume the auth plugin in an everything-dev app | [`better-near-auth#auth-plugin`](/skills/better-near-auth/auth-plugin/SKILL.md) | +| Integrate better-near-auth with TanStack Router (SSR or CSR) | [`better-near-auth#tanstack`](/skills/better-near-auth/tanstack/SKILL.md) | +| Start dev servers, debug hot reload, understand the service-descriptor architecture | [`everything-dev#dev-workflow`](/skills/everything-dev/dev-workflow/SKILL.md) | +| Debug the `extends` chain, deep merge semantics, resolved-config lifecycle | [`everything-dev#extends-config`](/skills/everything-dev/extends-config/SKILL.md) | +| Scaffold a new app with `bos init`, sync from upstream, upgrade framework packages | [`everything-dev#init-upgrade`](/skills/everything-dev/init-upgrade/SKILL.md) | +| Publish the resolved config to FastKV, deploy, sync, upgrade | [`everything-dev#publish-sync`](/skills/everything-dev/publish-sync/SKILL.md) | +| Read and write the FastKV config registry — key layout, namespaces, integrity | [`everything-dev#registry`](/skills/everything-dev/registry/SKILL.md) | +| Build shared-host, shared-API super apps with tenant-specific UI composition | [`everything-dev#super-app`](/skills/everything-dev/super-app/SKILL.md) | +| Follow code style — naming, semantic Tailwind, no comments, import conventions | [`everything-dev#code-style`](/skills/everything-dev/code-style/SKILL.md) | +| Look up any `bos` CLI command — flags, options, environment settings | [`everything-dev#cli-reference`](/skills/everything-dev/cli-reference/SKILL.md) | + +Two skills are read together more often than not: + +- A plugin's shape in this repo ([`everything-dev#plugin-development`](/skills/everything-dev/plugin-development/SKILL.md)) and the machinery underneath it ([`every-plugin#plugin-development`](/skills/every-plugin/plugin-development/SKILL.md)). Start with the first; open the second when a build or runtime detail doesn't match. +- A new API endpoint ([`everything-dev#add-a-route`](/skills/everything-dev/add-a-route/SKILL.md)) and its auth middleware ([`everything-dev#api-and-auth`](/skills/everything-dev/api-and-auth/SKILL.md)). Open the second when the route needs a session, role, or API key. + +## Facts every skill relies on + +| | local dev | production | +|---|---|---| +| App origin | `http://localhost:3003` | your runtime domain | +| API base | `http://localhost:3001` | same origin as the app (`/api/*`) | +| MCP endpoint | `POST http://localhost:3001/api/mcp` | `POST /api/mcp` | +| Skill family | served by the UI dev server | `/skills///SKILL.md` | +| Auth header | `x-api-key: api_...` (create at `/settings/api-keys`) | same | +| Runtime config | authored `bos.app.ts`, resolved under `.bos/` | published to FastKV (`apps///bos.config.json`) | + +There is no environment parameter anywhere — the origin is the choice. + +## Clone and modify -```bash -bos dev --api remote # isolate UI work -bos dev --ui remote # isolate API work -bos start --no-interactive # production URLs -``` - -### Edit the UI - -- main UI code lives in `ui/src/` -- routes live in `ui/src/routes/` (TanStack file-based router; `routeTree.gen.ts` regenerates automatically) -- reusable components live in `ui/src/components/` (`@/components` barrel) and `ui/src/components/ui/` (primitives like `button`, `select` — import these directly) -- runtime helpers live in `ui/src/app.ts` (`getAppName`, `getAccount`, `getActiveRuntime`, `getRuntimeConfig`, `useApiClient`, `useAuthClient`) -- use semantic Tailwind classes: `bg-background`, `bg-card`, `text-foreground`, `text-muted-foreground`, `border-border`. No hardcoded colors. - -### Edit the API / plugins - -- API shell contract: `api/src/contract.ts` (oRPC route definitions + Zod schemas) -- API shell router: `api/src/index.ts` (`createPlugin`) -- Plugins live under `plugins//` with `contract.ts` + `index.ts` + rspack config -- UI calls plugins via namespaced clients: `apiClient.template.listThings(...)`, `apiClient.registry.listRegistryApps(...)`, etc. - -### Publish - -```bash -bos build # build all workspaces (staleness-checked prerequisites first) -bos deploy # full train: preflight → build → upload bundles → publish config → image → Railway -bos publish # config-only: re-publish the resolved config to FastKV -bos sync # sync from upstream -``` - -Published config is authored in `bos.app.ts`; the resolved `bos.config.json` under `.bos/` is generated and never committed. - -### Regression tests +This repo is the **everything.dev base runtime** — the source of truth for the published framework packages (`every-plugin`, `better-near-auth`, `everything-dev`), the universal runtime image, and the everything.dev app itself (host, slim UI/API shells, plugins). It is not a generated child project. -The repo has a Go-based regression test suite under `tests/regression/`. These cover boot surface, auth flows, CORS, OpenAPI, security headers, and plugin registry. Keep them solid. +After cloning, read **`AGENTS.md`** at the repo root — operational guidance, the TanStack Intent skills block, and regression test instructions. Then load the skill for the task in front of you from the table above. -```bash -cd tests/regression && go test ./http/ -v -``` +To scaffold a **new app** instead of modifying this one, use `bos init` — `everything-dev#init-upgrade` walks it. -### Good tasks for an agent +## Rules to work by -- add or edit a public route under `_public/` -- wire a new API endpoint into `contract.ts` + `index.ts` and call it from the UI via `useApiClient()` -- add a plugin under `plugins/` and register it in `bos.app.ts` -- publish a UI update without changing the shared host -- debug why a composed remote is not loading +- **Never edit `*.gen.*` files.** They are regenerated by `bos dev`, `bos build`, and `bos typecheck`. +- **Build through the train.** `pnpm run build [target]` — raw per-workspace builds bypass the prerequisite graph and are unsupported. +- **Handlers are Effect.** New routes use `.effect(function* ...)` generators and access services with `yield* Tag`; no new plain `.handler(async)` routes with inline service access. +- **Semantic Tailwind only.** `bg-background`, `text-muted-foreground` — never hardcoded colors. Files are kebab-case. No comments in implementation. +- **Typecheck before you publish.** `pnpm run typecheck` generates types first and is self-sufficient. +- **Authored config is `bos.app.ts`.** The resolved config under `.bos/` is generated; never commit it. ## Public entry points -- `/` -- `/about` -- `/skill` -- `/skill.md` -- `/llms.txt` -- `/.well-known/mcp.json` -- `/.well-known/version` — deployed fingerprint + per-slot pins + last watch-tick outcome -- `/api` (OpenAPI docs) -- `/api/spec.json` (OpenAPI spec) -- `/api/mcp` (MCP server) +- `/` — landing +- `/about` — renders the repo README +- `/skill` — this page rendered +- `/skill.md`, `/llms.txt` — raw doc endpoints +- `/skills///SKILL.md` — the skill family +- `/.well-known/mcp.json` — MCP discovery +- `/.well-known/version` — deployed fingerprint + per-slot pins +- `/api` (Scalar docs), `/api/spec.json` (OpenAPI), `/api/mcp` (MCP server) diff --git a/ui/src/routes/_public/skill.tsx b/ui/src/routes/_public/skill.tsx index 311151343..9f675687f 100644 --- a/ui/src/routes/_public/skill.tsx +++ b/ui/src/routes/_public/skill.tsx @@ -22,6 +22,35 @@ const INTENT_COMMAND = "npx @tanstack/intent@latest load everything-dev"; const INTENT_REGISTRY_URL = "https://tanstack.com/intent/registry/everything-dev"; +const SKILL_FAMILY = [ + { + pkg: "everything-dev", + skills: [ + "talk-to-the-app", + "add-a-route", + "ui-integration", + "plugin-development", + "api-and-auth", + "dev-workflow", + "extends-config", + "init-upgrade", + "publish-sync", + "registry", + "super-app", + "code-style", + "cli-reference", + ], + }, + { + pkg: "every-plugin", + skills: ["plugin-development", "plugin-client", "plugin-testing"], + }, + { + pkg: "better-near-auth", + skills: ["auth-plugin", "client", "siwn", "relay", "subaccount", "tanstack"], + }, +]; + export const Route = createFileRoute("/_public/skill")({ loader: async ({ context }) => { const runtimeConfig = context.runtimeConfig; @@ -151,6 +180,47 @@ function SkillPage() { +
+

The skill family

+

+ Load ONE skill for the task in front of you — each covers its common case and points to + references for the rest. Served at + + /skills/<package>/<skill>/SKILL.md + + or loadable with TanStack Intent. +

+
+ {SKILL_FAMILY.map((group) => ( +
+

{group.pkg}

+ +
+ ))} +
+
+
{skill ? (