Skip to content

TryPost AGENTS.md changelog

io.github.trypostit/trypost

1 change recorded. MCP Harbor re-reads this listing from the official registry every six hours and asks the live server what it exposes, and keeps what changed between one look and the next.

  • AGENTS.md +79 −25

    https://raw.githubusercontent.com/trypostit/trypost/HEAD/AGENTS.md

    − The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.− This application is a Laravel application running on PHP 8.5. You are an expert with the Laravel ecosystem. Always use the APIs that match the installed major version of each package — do not assume a version.− - If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them.− ## Replies− - Be concise in your explanations - focus on what's important rather than explaining obvious details.− - Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.− - This project keeps committed, area-grouped rules in `.ai/rules` (settled decisions, non-obvious traps, standing constraints). Framework and package guidelines that only apply to specific paths (testing, frontend, components) also live there, under `.ai/rules/boost` — this is not just recorded decisions, it is load-bearing guidance you have not seen inline. Before you enter plan mode or create/edit any file, you MUST first: open @.ai/rules/index.md (it maps file globs to rule files), read every− - Record durable rules with `record-rule` so the next agent or teammate inherits them instead of working them out again. Pass a `glob` (e.g. `app/Http/Controllers/**`), a short `title`, and a few-line `note`. Always use `record-rule`, never your native memory or notes tool — native memory is personal and session-scoped; only `.ai/rules` is shared with the team and persists in the repo.− - Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.− - Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter.− ## Vite Error− - If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.− ## Pest− - This project uses Pest for testing. Create tests: `php artisan make:test --pest {name}`.− - The `{name}` argument should not include the test suite directory. Use `php artisan make:test --pest SomeFeatureTest` instead of `php artisan make:test --pest Feature/SomeFeatureTest`.− - Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.− - Do NOT delete tests without approval.− - An owner without app access is always sent to the plan step (`EnsureAccountReady`, the MCP step, the old subscribe route): it sends them back to the first unanswered step, so a new owner still starts at persona while a former customer lands on the plan directly.− - An offer with `requires_card = false` and `trial_days` (`Offer::startsWithoutCard()`) never opens Stripe: the Welcome plan step starts Cashier's generic trial (`StartGenericTrial`: `accounts.trial_ends_at`, the Socials `plan_id`, the redemption, `trial.started`) and goes on to the MCP step (owner decision October 2026). The account has app access while it lasts (`Account::hasAppAccess()` = subscribed or `onGenericTrial()`), billing shows the trial and **Subscribe now** (`app.billing.checkout`)−   `userNow()`, never the browser clock. Times shown next to display-zone cards go− ## Icons (@tabler/icons-vue)− - Brand logos come from `@thesvg/vue`, one import per logo (`import Canva from '@thesvg/vue/canva'`). A one-color logo that must follow the theme goes through `inTextColor()` (`resources/js/lib/brandLogos.ts`).− - **Per account, not per platform.** Read the cap through `SocialAccount::maxContentLength()`. It is the platform's `Platform::maxContentLength()`, except an X account with long posts (`hasXLongPosts()`: a `Basic`, `Premium` or `PremiumPlus` subscription, or `verified_type=business`), which gets 25000. The tier lives in the account meta and is written by `SyncXSubscription`, called from `ConnectionVerifier` on connect and on the daily `social:check-connections`.− - Bluesky, Mastodon and X (`App\Support\ThreadReplies`), up to 24 replies in `meta.thread_replies`. Each reply is measured against the **account's** limit (`SocialAccount::maxContentLength()`), so an X account with long posts gets 25000 per reply; pass the account to `ThreadReplies::violation()` / `PostPlatformMetaRules::requiredMetaViolation()` whenever it is known.− - Each reply is `{text, media}` (a stored plain string is a text-only reply; `ThreadReplies::of()` normalizes both, `PostPlatformMetaRules::normalize()` stores objects). Its media belongs to that reply only, never to the root: it publishes as `ThreadReplies::replyContentType()` (X → `XPost`, Bluesky → `BlueskyPost`, Mastodon → `MastodonPost`), so it follows that type's media rules (`ThreadReplies::mediaErrors()`, at save and at publish), and a reply needs text or media. The frontend mirror is `T+ This application is a Laravel application running on PHP 8.5. Always use the APIs that match the installed major version of each package — do not assume a version.+ - If a frontend change doesn't show in the UI or you get a "Unable to locate file in Vite manifest" error, run `npm run build` or ask the user to run `npm run dev` or `composer run dev`.+ - Use `search-docs` before changes that depend on Laravel ecosystem APIs, behavior, configuration, or version-specific syntax. Skip it for copy-only edits and other changes where package documentation is irrelevant. Reuse sufficient results already in context instead of searching again.+ - This project contains committed, area-grouped rules in `.ai/rules` when that directory exists, including path-scoped framework guidelines under `.ai/rules/boost`. Before you enter plan mode or create/edit any file, you MUST first: open @.ai/rules/index.md (it maps file globs to rule files), read every rule file whose globs cover the path(s) in scope, and run `grep -rin 'keyword' .ai/rules` to catch what a path match alone misses. Do not write code until you have read and are following every ma+ - Add or update tests for behavior and logic changes when a test provides meaningful regression coverage.+ - Pure copy, styling, and layout-only changes do not require new or updated tests.+ - When test coverage applies, run the affected tests and ensure they pass.+ - Test the changed behavior and its important failure modes, but do not add tests beyond them.+ - Read the `testing-best-practices` skill before writing tests.+ # Pest+ - This project uses Pest. Create tests with `php artisan make:test --pest {name}`.+ - Do not include the test suite directory in `{name}`. Use `SomeFeatureTest`, not `Feature/SomeFeatureTest`.+ - Read the `testing-best-practices` skill for guidance on coverage, naming, structure, dependency isolation, and review.+ - Do not delete tests or test files without approval. They are part of the application.+ ## Running Tests+ - Run the narrowest set of tests that covers the change. Pass a file path or `--filter=testName` to `php artisan test --compact`.+ - Rerun a test after each change to it.+ - Run `vendor/bin/pest` to call the test runner directly. It accepts the same file path and `--filter=testName` arguments.+ - After the feature tests pass, ask the user to run the complete suite with `php artisan test --compact`.+ - **TypeScript only, never JavaScript** (owner decision November 2026). Every script, config and test harness is a `.ts` file: `eslint.config.ts`, `vite.config.ts`, the Node harnesses in `tests/fixtures/*-harness.ts` (Node strips the types when it runs them, so they need no build step). Never add a `.js` or `.mjs` file. The one exception is the Maizzle email project (`maizzle/*.js`), which keeps its own JavaScript config.+ - **Imports go through the `@/` alias** (`import { formatNumberCompact } from '@/lib/utils'`), never a relative path (`./`, `../`), even for a sibling file. ESLint refuses relative imports (`no-restricted-imports`), with no exception: the stylesheet is loaded by `@vite` in `app.blade.php`, not imported from `app.ts`. CI runs Prettier (`prettier-plugin-organize-imports` reorders imports) before ESLint, so never rely on an `eslint-disable-next-line` above an import.+ - **Value checks go through `@/lib/helpers`** (owner decision November 2026), the front-end mirror of Laravel's helpers, so a check reads the same on both sides: `blank()` / `filled()` for emptiness, `isEmpty()` (Laravel's `isEmpty()` on a string or a collection) when spaces count, instead of `=== ''` or `.length === 0`, `isString()` / `isNumber()` instead of `typeof x === 'string'`, `transform(value, callback, fallback)` instead of `value ? callback(value) : fallback`, and `clamp()` (Laravel's + - **A translated sentence with markup (bold, italic, a link) goes through `TransSlots`** (`resources/js/components/TransSlots.vue`, owner decision October 2026), our take on vue-i18n's `<i18n-t>` component interpolation for laravel-vue-i18n's `:placeholder` syntax. The lang string holds plain text and `:placeholders` only, in all 16 locales; each placeholder is filled by a named slot, so the markup lives in the template and every value is escaped by Vue: `<TransSlots :message="$t('calendar.resch+ ## Browser storage (`localStorage`)+ Everything the app keeps in the browser for a user lives in one key per user, `trypost:<user id>`, through `resources/js/lib/browserStorage.ts` (owner decision November 2026): `{ "version": 1, "user": { … }, "workspaces": { "<workspace id>": { … } } }`. `user` holds what applies in every workspace, `workspaces` what only makes sense in one (a channel id, a draft). Never add a `localStorage` key of your own.+ - Add a new item to the typed `UserStorage` (every workspace) or `WorkspaceStorage` (one workspace) interface in `types/browser-storage.ts` (named `area.thing`, like lang keys; `BrowserStorage` there is the whole stored value) and use it through `useUserStorage(key, default)` or `useWorkspaceStorage(key, default)` (`composables/useBrowserStorage.ts`): a writable ref read once per tab and shared by every component that uses the item, saved on set, following the user's other tabs, and following th+ - The store does the safe read and write: a missing, unreadable or other-version key (`BROWSER_STORAGE_VERSION`) reads as empty, a block that is not an object reads as empty, a browser without storage works on the defaults, and every write reads the key again first so two tabs never write back a stale copy.+ - The one exception is `trypost.media-source-popup`: not storage but a message the Canva / Google picker popup writes and removes at once, heard through the `storage` event on that key.+ - Browser tests read and seed it through `browserStorageItem($page, $user, $item, $workspaceId = null)` and `setBrowserStorageItem(...)` in `tests/Pest.php`, never the raw key. The earlier keys (#461) are moved in once by `lib/legacyBrowserStorage.ts` (owner decision November 2026): before the first read of the storage on a page with a current workspace (`useBrowserStorage.ts` and the composer draft call it, so the values a page shows right after sign-in are already moved), it copies the signed-+ ## Inviting a user to connect an AI (MCP)+ There is no MCP step in Welcome (owner decision November 2026): it was friction before the user ever saw the app, and phones could rarely finish it. Processing always opens the schedule. Instead, `components/mcp/McpPromptCard.vue` (mounted in `AppSidebarLayout`) invites the user from inside the app:+ - **Shown** while the shared `mcpPrompt` prop is set (`HandleInertiaRequests::mcpPrompt()`, with the MCP URL the steps show: a current workspace whose account has app access, or self-hosted, and no AI of the user connected to that workspace (`HasConnectedMcpClient`, the rule the MCP settings list follows); the connection is cached five minutes per user and workspace, while app access is read on every page, so the card and the referral survey show on the first page after an account gets app acces+ - **Closing it** (the X, labeled "Not now") is kept in the browser storage (`mcp.promptDismissed`, see "Browser storage"): no database column. Another browser shows it once more. Connecting an AI hides it everywhere, because that comes from the server.+ - **Browser tests** see the card on almost every page (a test user has app access and no AI). A test whose page should not carry it calls `connectAiTo($user)` (`tests/Pest.php`), which connects an AI to the user's current workspace; never hide it through the browser storage.+ - **The card** shows the best-known apps as overlapping avatars (ChatGPT first, on top), and two buttons in the brand colors of Claude and ChatGPT; the close button is the way out. Each opens that app's connect steps (`McpConnectDialog`) on the page the user is on. While the card or its steps are on screen, the card asks `app.mcp.prompt.show` every two seconds, so an AI connected from anywhere (the card, the MCP settings, another tab) hides it; only `connected: true` hides it with confetti (as t+ - **Tracking:** each answer posts to `app.mcp.prompt.store`, which dispatches `mcp.prompt_clicked` (with the `client` picked: `claude`, `chatgpt` or `other`) or `mcp.prompt_dismissed` (`App\Enums\Mcp\PromptAnswer`). The connection itself is `mcp.connected` (`TrackMcpConnection`), whatever led to it.+ - An owner without app access is always sent to the plan step (`EnsureAccountReady`, the old subscribe route): it sends them back to the first unanswered step, so a new owner still starts at persona while a former customer lands on the plan directly.+ - An offer with `requires_card = false` and `trial_days` (`Offer::startsWithoutCard()`) never opens Stripe: the Welcome plan step starts Cashier's generic trial (`StartGenericTrial`: `accounts.trial_ends_at`, the Socials `plan_id`, the redemption, `trial.started`) and goes on to Processing (owner decision October 2026). The account has app access while it lasts (`Account::hasAppAccess()` = subscribed or `onGenericTrial()`), billing shows the trial and **Subscribe now** (`app.billing.checkout`), +   `userNow()`, never the browser clock. `userNow()` is a wall clock in the user's zone, not an instant: to ask whether a UTC instant has passed (`date.hasPassed()`), compare instants with `dayjs()`, or a browser in another zone than the user's is off by the difference. Times shown next to display-zone cards go+ ## Icons (@tabler/icons-vue and @thesvg/vue)

    Showing the first 40 of each.