Commit Graph
121 Commits
Author SHA1 Message Date
PiEgg a695d1987f chore(v3): use public npm and unify workflow naming (#10588) 2026-10-09 03:33:32 -04:00
YANG QIA bc1e83ffac feat(plugins): update the plugins, templates and UI Library for the new runtime (#553)
* Update the plugins, templates and UI Library for the new runtime

Device authorization, scoped API keys, service accounts, user invitations and preferences, secrets-backed credentials, the inbox and the cn package across the existing plugins, examples and the three templates, with the UI Library's auth, inbox, authorization and component items.

* Declare the UI Library site's favicon

* Use the NocoBase favicon on the UI Library site

* Fix generated-app shell tests and the Hub's user deletion test

Inline the authentication client in the templates' Vitest config so its guards share the test's react-router, and update the Hub test to the users plugin's new copy and spinner label.
2026-10-08 01:10:33 +08:00
YANG QIA 37c8d20aa6 feat(app): add the runtime, CLI client and agent runner for agent-run applications (#552)
Encrypted secrets, a command manifest derived from the API document, environment-variable manifests, sample data and on-demand Apps in the runtime, plus the new agent protocol, application CLI client, agent runner and Docker App Host packages.
2026-10-07 23:54:20 +08:00
Drol d631536271 feat(app-plugin-ai-employee): grant AI settings per page under System management, and drop unused management routes (#548)
Replace the single `page:ai.settings` grant with seven settings items under
System management → AI, one per settings page: `ai.employees`,
`ai.llmServices` and `ai.mcpServers` with `read` and `manage`, and
`ai.skills`, `ai.tools`, `ai.usage` and `ai.conversations` with `read`.
Every management route names the permissions it accepts, pages open with
`read` and turn their controls off without `manage`, and previewing another
user's AI file requires `read` on `ai.conversations`.

A migration gives every item and action to each Permission Set that held the
page grant. A grant of every page no longer opens the AI settings.

Remove the management routes no settings page called: employee templates,
create and delete, skill and tool writes, single LLM service and MCP server
reads, and both MCP connection tests. The package moves to the 3.0.0
prerelease line.
2026-10-07 09:53:24 -04:00
PiEgg 0b933b39c3 feat: generate the OpenAPI document and serve Swagger UI (#533)
Generates an OpenAPI 3.1 document for every application at GET /api/swagger and serves Swagger UI at GET /api/swagger/docs to signed-in users and API keys. Every hand-written /api route is declared or explicitly hidden, data endpoints are documented per field, Better Auth's endpoints are merged, routes behind runtime dispatchers are registered generically, and pnpm openapi:check enforces the declarations in CI. All changed packages carry changesets.
2026-10-05 09:23:03 +08:00
YANG QIA 7e5b7d4e72 refactor(templates): build the sidebars from shadcn Sidebar primitives (#534)
* refactor(templates): build the sidebars from shadcn Sidebar primitives

Replace the hand-rolled LayoutSidebar and NavigationTree in the default,
examples and hub templates with shadcn's Sidebar, keeping the collapsed
menu behaviour from #317: zero-delay tooltips, hover and keyboard-focus
popovers with permission-filtered children, nested expansion, parent
navigation and the selected group's highlight.

components/ui/sidebar.tsx stays as the shadcn CLI writes it. The layouts
customize it from outside: widths through the provider style in spacing
units, a phone Sheet with a translated title rendered by app-sidebar.tsx,
and a document keydown listener that keeps Ctrl/Cmd+B from reaching the
provider's window listener.

Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2

* fix(templates): tidy the sidebar footer and add the edge rail

Space the footer like the menu above it: the shield aligned to the first
line, the slogan's two lines, then the name and version on their own
truncated lines in the body font. In icon mode only the shield remains,
with a tooltip carrying the same text.

Add shadcn's SidebarRail with a translated label and title, passed as
props that override the primitive's built-in "Toggle Sidebar".

Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2

* refactor(ui-library): split auth-ui into presentational auth components

Replace the auth-ui block with four presentational registry components built
from shadcn primitives: auth-forms (Field, InputGroup, Alert), auth-methods
(Tabs, FieldSeparator, Button), auth-centered-layout and auth-split-layout.
They call no plugin and take every string as a prop with an English default.

The templates' sign-in, sign-up and password pages become application code
that wires the forms to the authentication plugin's headless actions,
translates them from client/locales, and keeps the NocoBase brand panel as
the split layout's aside. The ui-library preview loads shadcn/tailwind.css
and the shadcn Label so the data-attribute variants render.

Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2

* fix(ui-library): keep auth demo links inside their demo

Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2

* refactor(templates): install the auth registry blocks under client/extensions

Claude-Session: https://claude.ai/code/session_01KLvKwu4oUSHnFgGAy3M4v2
2026-10-05 04:54:36 +08:00
PiEgg 21d274cc71 refactor: move every existing /api route onto the HTTP API specification (#532)
Moves every /api route in the repository onto the HTTP API specification: camelCase plugin namespaces, standard methods and slash-separated custom methods, { data } / { data, meta } responses, the standard error body with reason and domain, permission before validation, side-effect-free GETs, Repository errors that declare their own status, save-time validation of write grants, duplicate route detection at start, and optional global API limits in config.yml. All changed packages carry changesets.
2026-10-04 16:39:20 +08:00
chenos e44f49cc5e feat(hub-cli)!: remotes, hub auth, build for the Hub, releases/status and resumable uploads (#498)
* feat(hub-cli)!: deploy to remotes, save keys with hub auth, build for the Hub

hub-cli keeps the Apps an application deploys to as named remotes in the
committed .nocobase/hub.json (`hub remote add/list/remove`), saves one API
key per remote in the user's configuration directory (`hub auth
login/logout/status`), and no longer reads HUB_URL, HUB_APP_ID, HUB_API_KEY,
the App root .env or the --hub/--app-id/--api-key flags. Every command takes
--remote.

`hub deploy` and `hub upload` ask the Hub which platform its Host runs Apps
on and run `nocobase build --target … --node-version … --tar` for it before
uploading; --no-build and --file upload an existing archive after checking
its recorded target. `hub deploy` uploads and then deploys through the
deploy endpoint, so an archive the Hub already has is deployed as its
existing Release.

The Hub's upload route only uploads: the configuration-prefixed body and the
deployment-intent and wait headers are gone. Uploads are checked against the
Host's build target, the App detail reports it, and which routes a
publishing key may call is declared with each route. The Host reports its
runtime platform in its status.

* feat(hub-cli): add hub releases and hub status

`hub releases` lists an App's Releases newest first with the platform each
archive targets, whether it runs now and whether it ever deployed, and
`hub status` reports the platform the Hub builds for, what the App runs and
its last deployment. Finding the Release to roll back to no longer needs the
Hub's web page.

The Hub opens its Release list, single Release and deployment list to
publishing keys holding either scope. The Release list takes a `limit`, each
Release reports `buildTarget`, `running` and `everDeployed`, computed from
existing records, and deployment list items carry `finishedAt`.

* feat(hub-cli): upload archives resumably

hub-cli sends the archive through the Hub's resumable upload: a session for
the archive's size and checksum, chunks in order at the offset the Hub has
reached, then completion. A chunk whose answer is lost is resent from the
offset the Hub reports, a session left unfinished by an earlier run is
resumed, and an archive the Hub already has is not sent again. Archives may
be up to 2 GiB, and a reverse proxy only needs to allow one 8 MiB chunk.

The Hub stages sessions on local disk under a new `uploadsDir` option,
which the Hub template sets to storage/hub/uploads, expires them 24 hours
after their last chunk, and creates the Release on completion through the
same checks as the single upload, which the management console keeps using.

* fix(hub-cli): name the command to run when no remote is configured

* feat(app-cli): let a CLI plugin describe its nested topics

`defineCliPlugin` accepts `topics`, a one-line description for each topic
nested under the plugin's own. Help showed one of a nested topic's commands'
summaries in its place, so `hub remote` read "Add a Hub App to deploy this
application to." A topic is described only where a command under it is
registered, and a described topic with no command under it is rejected.
hub-cli describes `hub remote` and `hub auth`.

* fix(hub-cli): address review findings on remotes, retries and deadlines

- Stop create-app from ignoring .nocobase/ in a generated application's
  .gitignore, so .nocobase/hub.json is committed as documented, and warn
  from hub remote add when an existing checkout still ignores it.
- Count a failed offset read as one of the consecutive chunk failures
  instead of ending the upload, and go on from the offset an
  UPLOAD_OFFSET_MISMATCH reports without a second request.
- Give each Hub request its own deadline, report one that outlasts it as
  TIMEOUT, and bound the wait for a deployment separately, so --timeout
  no longer caps a whole resumable upload.
- Report a key as rejected in hub auth status only when the Hub rejects
  it; anything else leaves it unchecked.
- Look remotes up as own properties, so a name like constructor is not a
  remote, and reject a default that is not a remote name.
- Settle the hidden prompt when standard input ends or errors.
- Stage upload sessions one directory per App, scan only the App's own
  sessions when an upload starts, and sweep every App's expired sessions
  at most once an hour.
- Drop the unreachable not-found branch from getReleaseSummary.

* fix(hub-cli): roll back under the default key, name a missing Hub, cancel login cleanly

- `hub deploy --release-id` of a Release the App deployed before and has since
  moved on from was answered with that earlier deployment, so the documented
  rollback deployed nothing and exited 0. A reused deployment that is not the
  App's latest now leads to the next key, derived from it; a retry walks the
  same keys and still repeats nothing. A given --idempotency-key stays exact.
- A 404 without a Hub error code, as at a mistyped mount path, fails with
  `HUB_NOT_FOUND` and suggests `hub remote list` instead of a bare
  `HUB_REQUEST_FAILED` or an "unknown result".
- Ctrl-C at the `hub auth login` prompt reports `LOGIN_CANCELLED` (exit 130)
  rather than `LOGIN_FAILED`.
2026-10-01 08:49:24 -04:00
Drol e77641ba3c feat(queue): rebuild @nocobase/queue on BullMQ 6.3.6 (#516)
* feat(queue): rebuild @nocobase/queue on BullMQ 6.3.6

Replace the @boringnode/queue wrapper with a per-application QueueService
whose producers, consumers and managers are bound to named queues. A
queueBackend of redis (or a registered BullMQ backend factory) runs on
BullMQ's public Queue and Worker APIs under a hash-tagged prefix; inMemory
runs in the process and persists unfinished jobs to a state file at
shutdown. Both implementations share one consumer layer and one contract
test suite.

- app-server: QueueServiceProvider, queueServiceToken and AppQueueConfig
  replace QueueProvider and its tokens; the queue migration source and
  planAppRuntimeDatabaseTasks are removed; queue.jobs is deprecated,
  ignored and reported once per plugin
- templates: compose QueueServiceProvider, declare memory and redis keys,
  drop the @/jobs alias
- create-plugin: remove the server.jobs capability
- examples: rewrite the queue example on the new API
- docs, Skills and AGENTS.md describe the new contract

* docs(jobs): make JobExecutor the default for background work

The previous commit rewrote every Queue Job reference onto the new queue
API, but most of what those jobs did belongs to @nocobase/jobs now.

- create-plugin: server.jobs generates a JobExecutor job and the provider
  that owns the plugin's executor instead of being removed
- plugin and application development Skills: choose JobExecutor for
  one-off work, ScheduleExecutor for recurring work, Scheduler when
  administrators track it, and a queue only for delays, priorities,
  deduplicating IDs, batches, rate limits and fan-out; jobs receive
  services by closure
- Scheduler Skill, evals and docs: hand a target's lengthy work to a
  JobExecutor with the occurrence's execution record as the reference,
  and let the job decide its terminal outcome; describe Scheduler's own
  jobs backend instead of the removed queue connection
- deployment and Notification Skills: cover the jobs and queue backends
  instead of a queue manager

* refactor(queue): select the implementation with adapter

queueBackend used to pick both the implementation and a BullMQ backend
factory. A configuration now selects the implementation with
`adapter: 'inMemory' | 'redis'`, and the optional `queueBackend` only
names a factory registered with `registerBackend()` for the redis
adapter, defaulting to BullMQ's Redis backend. A registered factory
interprets its own connection, so `connection` is required only on the
Redis backend. `inMemory` is no longer a reserved factory name.
2026-09-30 11:46:47 -04:00
Zeke ZhangandClaude Opus 5.5 7534fb6b99 refactor(templates): add shadcn/ui primitives on demand with an embedded shadcn/ui skill (#464)
* docs(skills): route frontend work through an embedded shadcn/ui skill

The application development Skill now carries the shadcn/ui skill from
shadcn 4.21.0, unchanged, under references/frontend/shadcn/, and
references/frontend/references/shadcn.md says what to read there and
where an application departs from it: `pnpm exec shadcn`, no `apply` or
`--preset`, overlays as child routes, labels above inputs, and the
English built into a few primitives translated.

- Primitives are added on demand with `yes n | pnpm exec shadcn add`,
  which keeps installed primitives from being overwritten and lets a
  non-interactive run finish, then formatted with Prettier.
- styling.md, form.md and the other topic references link to the skill
  instead of restating it; the worked example names the primitives each
  file needs on an **Add first** line.
- The frontend handbook rework: the projects worked example split into
  one document per file, shell.md, the capture tool guide and the
  reviewer prompts.
- Both .prettierignore files skip the vendored skill so a shadcn upgrade
  can replace it wholesale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(app-template-default): ship only the primitives the shell uses

Primitives are added with the shadcn CLI when a page first needs one,
so every application, template and plugin starts from the same CLI
output. The shadcn/ui skill embedded in the development Skill replaces
the reference gallery.

- Remove client/pages/reference/ with its tests, the 49 primitives that
  neither the shell nor the shipped compositions import, and
  use-mobile.ts. The template keeps button, calendar, dialog,
  dropdown-menu, input, label, popover, select, spinner, table, toast
  and tooltip.
- Drop the devDependencies only those primitives used (recharts, cmdk,
  embla-carousel-react, input-otp, react-resizable-panels,
  @shadcn/react) and add cn, which registry primitives now import.
- toast.tsx names its close button through actions.close, as dialog.tsx
  and spinner.tsx translate theirs; primitive-labels.test.tsx fails
  when an update brings the registry's English back.
- DataTable takes showSelectedCount, so a table without row selection
  hides "0 of N row(s) selected".
- Add the page test harness and application locale coverage tests; the
  client routes test pins page grants by their authz resource id.
- AGENTS.md and CLAUDE.md send UI work to ui-workflow.md, list the
  shipped primitives, and describe adding the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(templates): ship only the primitives Hub and Examples use

Hub and Examples follow Default: primitives are added with the shadcn
CLI when a page first needs one, and the shadcn/ui skill embedded in the
development Skill replaces the reference gallery.

- Remove client/pages/reference/ with its tests and use-mobile.ts from
  both templates. Hub keeps the twelve primitives its shell and
  compositions use; Examples also keeps badge, card, field, separator,
  skeleton, textarea, toggle and toggle-group for its example pages.
- Drop recharts, cmdk, embla-carousel-react, input-otp,
  react-resizable-panels and @shadcn/react, which no template imports
  now, and add cn, which registry primitives import.
- Carry over Default's toast close label, DataTable showSelectedCount,
  the page harness, locale coverage and primitive label tests, and the
  AGENTS.md and CLAUDE.md guidance, keeping each template's own route
  grants and wording.
- Add the changeset for all three templates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): name the primitives each template ships

The frontend handbook said the template ships twelve primitives, which
holds for Default and Hub but not for Examples. shadcn.md now lists the
eight Examples adds, and frontend-dev.md and example.md refer to the
primitives the shell uses instead of a count. Add the changeset for the
embedded shadcn/ui skill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): open records in place, add the back button and the dashboard template

A review of a customer-service application built from the handbook found records opening over the wrong page,
a row's Edit opening the drawer as well, a back button among the page actions, an unaligned table in a card and
unsortable date columns. The handbook now says a record opens over the page the user is on (I9), declares the
drawer under every page that opens it, gives a row's Edit its own route, puts BackButton above the title (L6),
makes date and number columns sortable (T1.8), and adds a dashboard template (T5) with a worked example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(templates): add BackButton and keep the page count on one line

BackButton is the way back from a page below another one: a link above the heading that leads to the parent
route with the query string and replaces the history entry, as closing a route overlay does. It stands on its
own, without PageHeader. DataTablePagination gives the page count a minimum width, so the Chinese label no
longer wraps. RouteChildPage and the UI Library describe leaving by the back link instead of breadcrumbs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(skills): show the success path in the unsaved-changes example

A generated application hit this exact bug: the edit dialog's save
cleared neither the dirty flag that beforeClose reads nor the form's
report, so closing after a successful save asked to discard the just
saved changes.

The overlay.md section 2.5 example now carries the whole flow: a stable
onDirtyChange callback, an EditProjectBody whose onSubmitted clears the
flag before onSaved and close, and the imports that go with it. The
frontend handbook's Common mistakes list gains the matching entry.

* fix(templates): keep the Switch and pie chart in proportion in the compact theme

* docs(app-skills): compare the whole design at acceptance and require consistent navigation icons

The acceptance review's D classification was scoped to the design's numbered
criteria, so a design's structural declarations — routes, navigation entries and
their icons, permissions, the component list and states — were only compared
with the code when they happened to be criteria. A trial feature lost two menu
icons this way and the acceptance pass did not catch it.

D now compares the whole design file with the code; the design template requires
every declaration in sections 2–4 to map to a criterion or be marked explicitly
as not a criterion; and the review checklist plus page.md require navigation
icons to be consistent within a group.

* docs(app-skills): open a record's detail under the page that opens it

I9 covered drawers and dialogs only, so a page that referenced a record whose
detail is a page (a payment list's expense number) linked into the other
module's route: the page the user was on disappeared, the menu highlight moved,
and Back landed elsewhere. The page version of "the same drawer over another
page" was undocumented, and page.md told every record page to hard-code `to`,
which blocks reusing one page module under several parents.

I9 now covers a covering child page as well, child-routes.md gains "The same
detail page over another page", page.md limits `to` to a page with one parent,
and the common-mistakes entry names the detail page next to the overlay URL.

* fix(templates): make BackButton a muted text link

* refactor(templates): group the DataTable components in one directory

* docs(app-skills): show a cut-off table cell's full content on hover

Guideline T1.11 requires a table cell that is too narrow for its content to show the full content on hover. table.md gives the pattern: the cut-off content in a Popover whose trigger opens on hover, since a Tooltip does not work well here. A cell does not wrap and the table widens to fit it, so the pattern applies only where a column limits its width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(app-skills): treat the worked example as a reference, not code to copy

Agents copied the projects example verbatim, carrying its names, fields and fixed values into features they did not fit. The frontend handbook now says the examples show the rules rather than the feature to build, and names the projects-domain values to decide again. example.md and styling.md no longer tell an agent to copy the files a task depends on; only session-expired-alert.tsx and use-url-search.ts, which are shared infrastructure, are still copied unchanged.

The acceptance review also checks the other direction of design conformance: names and copy keys from the projects domain, fields, columns, filters or actions the design does not declare, and example values that do not fit the feature's data are reported as D issues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(app-skills): end a cut-off cell in an ellipsis and reveal it in a tooltip

A generated articles table clipped long titles mid-character and showed "release-notes" as "elease-note": the title used line-clamp in a cell that never wraps, and the tags put truncate on Badge, a flex element that shows no ellipsis and loses both ends of its centered text.

Guideline T1.11 now requires the ellipsis and a reveal on hover only when the content is cut off. table.md shows text and links, the name column included, in a Tooltip whose onOpenChange cancels it when the value fits; the tooltip trigger leaves a Link a link, which a Popover trigger would turn into a button. Styled content such as a Badge stays in a Popover, since a badge is unreadable on the inverted tooltip. It also says where truncate works and that line-clamp does not belong in a cell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(templates): line DataTable up with a card by itself

A generated analytics page put a DataTable in a CardContent with px-0, as the handbook's "Table in a card" said, but not the cell padding that went with it. Its own border then sat on the card's edge, a strip was left below it, and the text started to the left of the card's title. The rule lived in four places and was still only half applied, because it asked the agent to pick a different component and hand-write selector classes.

DataTable now does it: inside a CardContent it drops its frame, reaches the card's edges and pads its first and last cells with --card-spacing, and a CardContent still given px-0 keeps working. The handbook builds a card's short list from DataTable with plain headers and no pagination, the dashboard example uses it, and guideline T1 says it covers list pages while a short list in a card follows T5.3, which is where the design had picked up sorting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(app-skills): record every review checklist item in the review record

Both reviews of a generated content application were non-independent, since the environment could not start subagents, and neither checked T5.3: the design review raised only B1-B4 and S1-S4, and the acceptance review widened a clipped card without asking why the table sorted. The prompts already said to go through the checklist item by item, but the record kept only the items that failed, so a skipped item looked the same as a passed one.

The review record now has a "Review checklist" section with one row per checklist item: pass, fail with the issue it raised, or not applicable with the reason. Both prompts and the checklist's own introduction say to fill it in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(app-skills): validate forms on submit, not as a field loses focus

A field error flashed for a frame whenever a dialog was closed without saving. Guideline T3.3 validated a field when it lost focus, so the handbook and its examples set useForm's mode to 'onTouched'; clicking Cancel, the close button or the backdrop takes the focus off the field first, and the error rendered while the dialog was fading out.

T3.3 now validates on submit, and a form closed without submitting shows no error. The handbook and the four examples leave mode at the default 'onSubmit', and it explains why the blur-based modes do not fit. After a failed submission a field is still revalidated as it changes, so its error clears once it is corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(templates): let a child page inside another one cover it whole

A covering page under a tab of a record's page can only render through the tab's outlet, inside the record page's RouteChildPage. That layer both positioned and scrolled, so the nested layer resolved inset-0 against it and scrolled out of sight with its content: in the recruitment application, after the candidate page had been scrolled 292px, the offer page opened at top -240 and showed only its footer over the candidate's details.

RouteChildPage is now two elements, the way the layout's content area is: the outer one positions and never scrolls, the inner one scrolls, with overscroll-contain so scrolling stops at the layer. A layer rendered inside another one anchors to that layer's outer element and covers it whole. Inside another layer it also makes everything around it inert up to that layer's scrolling element — the record page's back link, header and tab bar — not only its own siblings. The layer carries data-slot='route-child-page'; the props are unchanged.

The three templates and the UI Library registry item carry the same file. The templates' AGENTS.md add that an overlay a page's header opens on a page with tabs is declared under every tab and linked through the current tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(app-skills): open a page header's overlays over the tab being shown

A review of the recruitment application found a record page whose header opened its edit dialog at /candidates/80/edit while the user was on /candidates/80/interviews. The handbook said a record opens over the page the user is on, but not that on a page with tabs that is the tab, and its two examples — the drawer with its stacked edit dialog, and the record page with tabs — never met. A link with a bare `edit` resolves against the route that renders it, not the URL on screen, and the header belongs to the page's route: the dialog replaced the tab in the page's outlet, and closing it went to the page's URL, which redirected to the default tab.

- Guidelines T2.1, I1, I6 and I9 define the view the user is on, the tab included, and I9 gives the test: an overlay's URL minus its own segments is the URL it was opened from, and closing returns there with no redirect.
- child-routes.md declares the header's overlays under every tab through customerHeaderRoutes(tab) inside customerDetailRoutes(owner), links the header through the tab read from the URL, and has each tab pass the page's context on to its outlet. page.md explains what a relative link resolves against, with a table of the cases.
- Going back restores the page underneath exactly (L6): links carry its whole query string down, a view below another one names its own parameters apart (table.md), and the way back removes them with withoutParams (example/url-search.md). child-routes.md no longer advises dropping the current page's filters when linking to another page's record, which lost them on the way back.
- example/detail-page-tabs.md is the complete page, a tab, a test that opens the header's "Edit" from every tab and goes back, and the route test helper; its code was compiled, linted and tested inside the default template.
- The review checklist, overlay.md, testing.md, frontend-dev.md and the run and design templates check overlays opened from a view other than the first one a page shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(templates): add DataTable and DatePicker from the UI Library on demand

The templates no longer ship DataTable or DatePicker, or the calendar,
select and table primitives only they used. Both are UI Library items
now, added with `shadcn add @nocobase/data-table` or
`@nocobase/date-picker` when a page first needs one. Examples keeps
select and table for its own pages, and Hub keeps react-day-picker,
which app-plugin-hub requires as a peer. The dataTable and datePicker
locale keys stay, so an item added later is translated at once.

- ui-library publishes data-table: DataTable and its column header,
  pagination and view menu as one item of four files under
  client/components/data-table/, with a preview demo and the
  dropdown-menu, select and table primitives it renders with.
- date-time-picker declares the calendar, popover and react-day-picker
  that the date-picker.tsx it installs imports; without calendar in the
  templates, installing it alone no longer compiled.
- ui-library gets Vitest. The DataTable and DatePicker tests move there
  from the templates and render the registry sources against the
  preview's primitives; Quality runs them on every pull request and the
  UI Library workflow before it publishes.
- The development Skill adds the items on the worked example's Add
  first lines and says which of the CLI's package.json changes to undo;
  the upgrade Skill covers an application that still has the old copies.
- The pending changesets that described a data-table directory in the
  templates now describe what ships.

* refactor(templates): drop the unused Typography components

client/components/typography.tsx held the class strings from the shadcn
Typography guide for hand-written long-form text. Nothing in the
templates or the plugins imported it, and article content usually comes
from Markdown or rich text, which these components cannot style. The
templates' AGENTS.md and the development Skill no longer list it.

* feat(ui-library): publish BackButton and keep it preinstalled in the templates

Make the UI Library the source of truth for BackButton, as it already is
for PageHeader and the route overlays.

- ui-library: add the back-button component item (back-button.tsx, its
  registry entry, a Navigation preview demo, and a test that renders it in
  the strict runtime built from the components README), document it in the
  components README with its navigation.back key, and use it in the
  RouteChildPage demo instead of a hand-written back link.
- templates: keep client/components/back-button.tsx as an exact copy,
  checked by tests/scripts/template-ui-library.test.mjs, and name
  BackButton among the UI Library components in AGENTS.md. The component
  itself is unchanged, and each template keeps its own back-button test,
  which is the one check that the template's own locales carry the key.
- MAINTAINING.md and USAGE.md list back-button among the preinstalled
  components, and MAINTAINING.md records navigation.back as the one
  exception to item-prefixed keys: it predates the item and pairs with
  the templates' navigation.breadcrumb.
- The development Skill's styling reference now says which composed
  components are preinstalled from the UI Library and which are the
  template's own, instead of saying the CLI cannot add any of them, and
  the plugin Skill lists back-button among the items a plugin copies.

* feat(ui-library): add a ConfirmDialog item built on alert-dialog

ConfirmDialog confirms one action in an alert dialog the page opens from
its own state. It runs the action: while a returned promise is pending
the confirm button shows a spinner and holds the focus, both buttons are
disabled and Escape is ignored; resolving closes the dialog with
onOpenChange(false, 'confirm'), and a rejection or a returned
{ error, retryable } keeps it open with the error inside it. Cancel
holds the initial focus, and focusAfterConfirm names where focus goes
after a success, when the opener has often gone with its record.

A result is reported to the onOpenChange of the latest render, and one
that arrives after the caller closed or unmounted the dialog is dropped,
so a handler whose close meant cancel is not called with stale state.
A ref, not render state, keeps two clicks from running the action twice.

The preview gains the alert-dialog and spinner primitives, a demo with a
slow, a failing and a refused delete, and the item's tests render it in
the strict i18n runtime built from the README's Translations table.

* docs(app-skills): fix the frontend guidance the PR review found wrong

- The toast examples in complete-button.md and public-switch.md, and the
  401 rule in api.md, used actionProps, left over from the Base UI toast
  API; the toaster's option is action: { label, onClick }.
- ui-workflow.md's development step still said to copy the worked
  example, as did frontend-dev.md and i18n.md in passing; they now say to
  write the feature's files from its design and copy only what a
  document calls shared infrastructure. i18n.md no longer lists the extra
  shared keys twice with different wording.
- The customer page test in detail-page-tabs.md assumed a mocked i18n
  client and queried keys; it renders in the strict test runtime with
  the application's locale files and queries the wording. The document
  also says detail/edit.tsx has to be written before the test imports it,
  with the customers.form keys copy.md does not list, and no longer claims
  the test checks what only the route test can.
- project-form.md and list-page.md name the select primitive they
  import, and server-table.md says what it needs and names
  data-table/index.tsx.
- The end-to-end guidance and the screenshot tool say what a Hub
  application sets up first, since the Hub template has no Playwright.
- shadcn.md no longer claims the application installs CLI 4.21.0.
- The docs site's child page example leaves by BackButton, says why a
  deeper outlet sits beside the layer as the component now behaves, and
  its shadcn add example no longer offers to overwrite the translated
  dialog primitive.
- form.md's create page is the frame to follow, not to copy.

* docs(app-skills): require RouteChildPage for a child route that is a page of its own

A generated application declared its article editor as a child route of a
dashboard and of a list and returned a bare PageContainer, so the editor
rendered at the parent's Outlet below the dashboard instead of covering it.
The handbook implied the rule without stating it.

- child-routes.md, page.md, form.md and frontend-dev.md state that only tab
  content renders inline and a page of its own returns RouteChildPage,
  including a form page shared by several parents.
- testing.md adds a covering child page test: the parent's heading sits in
  an inert element, which fails for a page rendered inline.
- The template AGENTS.md files carry the rule, since they are always read.
- The UI Library README and the docs site place the parent's Outlet at the
  end of its PageContainer, as the handbook does.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:10:19 -04:00
Junyi dfdd44902f refactor(workflow): refactor dsl (#478)
* feat(workflow): add typed DSL refactor demo

* refactor(plugin-workflow): refactor dsl

* fix(workflow): fix test case

* refactor(workflow): update skills

* fix(workflow): fix test case

* refactor(workflow): change unpublished route to hash
2026-09-29 23:45:16 +08:00
Katherine 23f052feab docs: move deployment to its own chapter and make it agent-first (#481)
* docs: move deployment to its own chapter and make it agent-first

Move the deployment section out of the development chapter into a
top-level `deployment/` chapter in both languages, and rewrite it around
two routes: handing the deployment to an AI Agent, with the session
location, Skill and prompt for each scenario, and doing it by hand, with
one page each for Hub and standalone deployment. Runtime configuration
and backup/troubleshooting stay as trimmed reference pages.

Update the home page cards, top navigation, development index and the
tutorial's Hub placeholder to point at the new chapter, and fix the two
links into the old path in the default template README and the
app-installer Skill.

* docs: fix deployment guidance links and rollback

* docs: make agent deployment prompts actionable

* docs: group deployment sidebar entries
2026-09-29 13:15:53 +08:00
Drol 414956d574 feat(ai-employee): discover and test models before startup (#483)
* feat(ai-employee): add model discovery and test commands

* docs(ai-employee): consolidate LLM configuration guidance
2026-09-28 12:44:17 -04:00
DrolandClaude Opus 5.5 aeff80a1d2 feat(jobs)!: add @nocobase/jobs and move Scheduler onto it (#463)
* feat(schedule): scaffold the @nocobase/schedule package (T001)

Adds packages/libs/schedule on the shared dev-config presets with BullMQ
6.3.6, cron and cron-parser as dependencies, lists it among the
isolatedDeclarations libraries, and records msgpackr-extract, which
arrives through BullMQ, as a deliberate build skip both here and in the
allowBuilds that create-app writes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(schedule): add the service contract, configuration fallback and validation (T002)

Exports the ScheduleExecuteService, ScheduleExecutor and job types from
the plan and createScheduleExecuteService. The service resolves a named
configuration, then schedule.default, then a built-in memory
configuration it reports through onFallback; returns one executor per
(scope, configuration); and rejects a repeated call whose overrides
change the effective concurrency or attempts. Scopes, job names and job
options are validated before anything reaches an adapter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(schedule): add the in-memory scheduler (T003)

Adds the executor shared by both adapters: it registers handlers, holds
jobs added before setup() and writes them there, skips a write whose
rule, payload and execution settings are unchanged, passes immediately
only for a new rule, keeps handlers after removeJob, fails firings
without a handler as handler-not-registered, and delivers subscriber
events in order.

The memory adapter plans firings with cron-parser and fires them through
one-shot cron jobs created from a Date. Every rule, its next firing and
its firing count persist to a versioned state file per namespace and
scope, written atomically; a missed firing runs once after a restart. An
exclusive lock file refuses a live holder or another host and takes over
a lock left by a crashed process. A cron rule without tz runs in UTC.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(schedule): add the redis adapter on BullMQ job schedulers (T004)

The namespace is the queue prefix and the scope the queue name. Rules
map to upsertJobScheduler with a template carrying the payload, attempts
and retention, compared through getJobScheduler before any write. A
worker runs firings with the configured concurrency and reports its
local active, completed and failed events; a firing without a handler
fails as an UnrecoverableError and is reported as handler-not-registered.
Shutdown aborts running handlers and waits for them before closing.

Only public Queue, Worker and Job API is used. The planned time is
job.timestamp plus the delay in job.opts, because BullMQ zeroes the
job's own delay once the job leaves the delayed set.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(schedule): run the redis adapter against a real Redis (T005)

Adds test:integration, which starts Redis in a disposable Compose
project, runs tests/integration against it and removes the project. The
suite covers one execution per firing across instances, continuing on
another instance after a shutdown, planned and next firing times,
skipping an unchanged write so a due firing survives, a changed rule
replacing it, immediately firing once, re-adding a removed job, a
missing handler failing without retry, retries, shutdown waiting for a
running handler, limit, and listing.

Measuring against BullMQ 6.3.6 changed two things in the adapters. An
end date already in the past is rejected by upsertJobScheduler, so both
adapters now remove such a rule instead of writing it. A scheduler whose
limit is spent keeps reporting its last planned time as next, so a next
firing that is not after the current one is reported as none.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(app-server): compose the schedule service under @nocobase/app-server/schedule (T006)

Adds scheduleExecuteServiceToken, AppScheduleConfig and
ScheduleExecuteServiceProvider. The provider builds the service from the
schedule section with the application name as the default namespace and
storage/schedule as the built-in memory location, reports a fallback to
that configuration outside develop and development, starts nothing, and
shuts down whatever executors their owners left running.
@nocobase/schedule is a peer, provided by the application.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(app-plugin-scheduler)!: schedule through @nocobase/schedule (T007)

Scheduler now hands each definition's rule to its own executor from
@nocobase/app-server/schedule, on the @nocobase/app-plugin-scheduler
scope with concurrency 1 and a single attempt, instead of the queue's
schedule projection and ScheduleDispatchJob.

- A new migration adds nextRunAt, lastRunAt, runCount, appliedLimit and
  lastOccurrenceId to scheduleDefinitions and scheduledAt to
  scheduleOccurrences; the published migration is unchanged.
- Startup registers every definition's handler, adding rules for the
  enabled ones, before setup(); sync-only mode sets the executor up
  without consuming and starts no reconciliation.
- A rule is rewritten only when its definition changed, carrying the
  limit left after the firings already run; disabling removes the rule
  and keeps the handler.
- The occurrence id is the firing's job id; starts count once per
  occurrence; a target that is not ready now records a skipped
  occurrence instead of failing with OCCURRENCE_NOT_FOUND.
- Shutdown clears the reconciliation timer, unsubscribes, and waits for
  the executor.

BREAKING CHANGE: ScheduleDispatchJob is no longer exported, the plugin
contributes no queue jobs, and nothing is migrated from queue_schedules
or queue_jobs. Applications must compose ScheduleExecuteServiceProvider
and provide @nocobase/schedule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(templates): compose the schedule service in every template (T008)

Each template adds ScheduleExecuteServiceProvider with the application's
NODE_ENV, a server/config/schedule.ts offering memory and redis
configurations without choosing a default, and @nocobase/schedule as a
production dependency, and drops the queue that existed only for the
Scheduler. config.example.yml documents the section, and the deployment
Skill covers choosing the backend for one or several instances.

Tests that start applications now keep schedule state in their own
temporary directory: two suites starting apps on the template's
storage/ at the same time are two processes on one memory state file,
which the adapter's lock refuses. The deployable dist records
msgpackr-extract as a deliberate build skip, as the repository and
create-app already do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor!: replace @nocobase/cron with @nocobase/schedule (T009)

Removes packages/libs/cron and the dependency the default and examples
templates declared on it; nothing imported it. The application
development Skill's section on work that runs on a schedule now uses an
executor from scheduleExecuteServiceToken under the application's own
scope, set up in start() and shut down in shutdown(), and explains that
the redis adapter, not a hand-written lock, is what keeps several
instances from firing the same job. The cron catalog entry stays for
@nocobase/schedule.

BREAKING CHANGE: @nocobase/cron is no longer developed here; its
published 0.1.0 remains installable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(scheduler): describe scheduling on the schedule service (T007)

The Scheduler capability page still described the queue's schedule
projection: a schedule queue worker, queue_schedules, a paused queue
schedule, and limit as queue claims. It now says where schedules run —
the schedule configuration, memory on one host unless schedule.default
names redis — in both languages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(changeset): describe the schedule service and the Scheduler move to it (T010)

Adds changesets for @nocobase/schedule and @nocobase/app-server, the
breaking Scheduler change with no migration from the queue tables, the
templates and app-skills replacing @nocobase/cron with
@nocobase/schedule, and the msgpackr-extract build skip in create-app
and app-tools.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(schedule): share the memory adapter's state across the processes of one host (T011)

The memory adapter held an exclusive lock for its whole lifetime, so a
second process — nb3 schedule:sync while the application ran, or a PM2
cluster — was refused. It now reads its state file without a lock, since
the file is replaced by rename and always whole, and takes a short
exclusive lock only for each read-modify-write, reading the file again
inside it. Each consuming process arms every planned firing and claims
it under the lock by advancing the rule past it, so a firing runs once
among them, and polls the file's signature to pick up rules the others
changed. A lock left by a process that no longer runs, or held far
longer than a write takes, is taken over.

Every write records a revision and its writer; a file another host
writes after this process started is refused, since a shared file system
gives neither atomic rename nor exclusive create reliably. The executor
passes its unchanged-rule check to the backend so memory decides it
again under the lock.

Documentation, Skills, the deployment guidance, the templates' comments
and the changesets now describe memory as one host rather than one
process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(schedule): keep the memory adapter's state in memory and write it on shutdown (T012)

The memory adapter now reads its state file once, in setup(), changes
only its memory while it runs, and overwrites the whole file in
shutdown(), after it stops firing and running handlers finish. This
replaces the shared, lock-guarded file of T011: there is no lock, each
process holds its own copy, and the one that shuts down last overwrites
the file. A process that ends without shutting down loses what changed
since it started, and a setup() that could not read the file never
overwrites it.

The lock module, the file's revision and writer, the signature poll,
the executor's unchanged-rule argument, the multi-process fixture and
its tsx dependency are removed. Documentation, Skills, deployment
guidance, template comments and changesets describe memory as one
process whose state reaches storage when the application stops.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(app-plugin-scheduler): remove stale rules again on every synchronization (T013)

Synchronization only ever added rules; it removed one only when it
deactivated a definition itself or an administrator disabled it. A
removal that was lost stayed lost: on the memory adapter a running
application overwrites the state file nb3 schedule:sync --finalize wrote,
so the rule of a definition removed from code came back and failed as
handler-not-registered at every firing.

Every synchronization now removes again the rule of each disabled
definition in the manifest and of each definition already inactive,
once the executor is set up. It reads the definition again first, so a
schedule another instance enabled in the meantime keeps its rule. An
active definition a partial manifest leaves out is still left alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(schedule): explain how applications and plugins use the schedule service

The README now covers when to use the package rather than the Scheduler
plugin or a queue job, how an application composes it, the schedule
configuration and how a consumer's configuration is chosen, the provider
pattern for plugins and application code with its scope, ordering and
shutdown rules, dependency declarations, job options and their edge
cases, receipts, handlers and orphan rules, events, both adapters'
guarantees and limits, and testing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(examples): add a plugin running a recurring job on the schedule service

@nocobase/app-plugin-schedule-example shows the pattern the schedule
README describes. Its provider takes an executor of its own from
scheduleExecuteServiceToken under its package name, subscribes to its
events, registers a heartbeat that runs every minute, sets the executor
up, removes the rules of jobs it no longer defines, and shuts the
executor down with the application. The handler records each run from
its own context, since events arrive after the fact. An authenticated
GET /api/schedule-example returns the next firing and the recent runs.

The examples template registers the plugin and covers the route end to
end on the application's schedule service.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: follow the consolidated app-cli after rebasing onto develop

@nocobase/app-tools is now part of @nocobase/app-cli, so the
msgpackr-extract changeset covers app-cli, whose dist workspace carries
the entry. The Scheduler's command is nocobase scheduler sync in the
changeset and the capability page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor!: rename @nocobase/schedule to @nocobase/jobs and let Scheduler choose its configuration

The package moves to packages/libs/jobs as @nocobase/jobs, its
configuration section from schedule to jobs, and the built-in memory
configuration's state from storage/schedule to storage/jobs, so the
section no longer reads like the Scheduler plugin's. Nothing was
published under the old name.

Scheduler gains a scheduler section: scheduler.jobs, or SCHEDULER_JOBS,
names the jobs configuration its schedules run on, and without it they
follow jobs.default. The jobs service would quietly fall back to the
default for a name it does not know, so Scheduler refuses to start
instead. The default and examples templates declare the section.

@nocobase/app-server/schedule and the ScheduleExecute* identifiers keep
their names for now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(jobs): report the planned time BullMQ stored in addJob receipts

upsertJobScheduler returns a job whose timestamp and delay come from two
clock readings, one in JavaScript and one in its script, so their sum
could miss the planned time by a millisecond even for a cron rule. The
receipt now reads the scheduler's own next, which is the planned time
itself, and falls back to the job only when there is none. The Redis
suite's check that a replaced firing runs at the receipt's time failed
on that millisecond.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(jobs): rename JobExecutorService and use configured executor settings

* fix(app-cli): resolve native platform packages from declared dependencies

* fix(jobs): exclude optional native accelerator and revert retarget changes

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 12:09:24 -04:00
DrolandClaude Opus 5.5 64cf25ac20 feat(ai-employee)!: key ai.llmServices by name and set secrets through config (#456)
* fix(ai-employee): drop obsolete SQL and naming rules from the system prompt

The database section of the system prompt depended on environment.database,
which no caller passes, so the quoting rules, the main database line and the
DB_UNDERSCORED snake_case rule never rendered. The unconditional "Database
Operations" rules told the model to write SQL against a <data_sources> tag
that is never emitted, while AI employees only read data through the
Repository-backed data tools, which take Collection and field names and apply
the Connection naming strategy themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ai-employee): remove the unused knownRoles option and getter

AIEmployeeService accepted a knownRoles option and exposed a getter that fell
back to the AI_DEFAULT_ROLES environment variable, but no caller passed the
option and nothing read the getter. Roles come from the requesting actor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ai-employee)!: key ai.llmServices by name and set secrets through config

ai.llmServices becomes a map keyed by service name, like ai.mcpServers, so
each service has a stable configuration path that `config set` and an `env`
mapping can address. The list form and a `name` field inside an entry are
rejected at startup.

The plugin no longer expands ${NAME} in ai.llmServices or ai.mcpServers. It
read those values from process.env, which a built server does not merge .env
into. A key is now written with `pnpm nocobase config set --from-env`, or,
when the environment injects it, mapped in `env` of server/config/ai.ts like
every other section's variables. `config check` therefore no longer exempts
the two AI sections from its literal-reference warning.

The templates default ai.llmServices to an empty map and declare
server/config/ai.ts in the object form with an empty `env`. The plugin
Skill, README and Chinese docs describe the map form and hand key handling
over to the application development Skill's `config set --from-env` flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(ai-employee): simplify key setup and model selection guidance

The Skill no longer walks an agent through building shell commands that
read a key. It tells the user the key's path and the `config set
--from-env` form, and leaves setting it to them.

Model selection now follows the order the key allows: the entry is written
without invented model ids, the user sets the key, and the models are
picked on the LLM services page, which fetches them from the provider with
the configured key. The agent no longer calls the provider itself.

Deployment configuration is handed to the nocobase-deployment Skill and the
deployment docs, a non-git application is covered where config.yml has to
stay out of the repository, the example service is named openai
throughout, and the templates' config.example.yml no longer claims that a
change applies without a restart. The changesets are rewritten to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ai-employee): check ai.llmServices in config check and warn about missing keys

defineAIConfig declares the ai section with the plugin's validation, the
way defineAuthConfig does for auth, and both templates now use it. With
it, `pnpm nocobase config check` reports a structural problem in
ai.llmServices as an error by path instead of letting the start fail on
it, and warns about a service whose built-in provider sends
options.apiKey and has none, with the `config set --from-env` command
that sets it. ollama and App-registered providers are not checked.

The structural checks now collect every issue rather than throwing on
the first, so startup and config check share them. The Skill, the
Chinese docs and the changesets describe running config check after
writing an entry and after the user sets the key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(ai-employee): use config check in the Skill's model setup

The shortest path now runs `pnpm nocobase config check` after writing a
service entry and relays its missing-key warning, path and command
included, to the user, then runs it again once the user says the key is
set. The completion checks require config check to report nothing under
`ai.`, and the Skill says to switch a plain defineAppConfig to
defineAIConfig before relying on the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 07:35:14 -04:00
Jiannx 8f187e204d docs: streamline Mail, Notification, and Template Print guides (#479)
* docs: simplify Chinese email, notification, and printing guides

* docs: localize email, notification, and printing guides
2026-09-28 16:43:21 +08:00
chenosandli'mac a857a087bb feat!: print the application CLI's --json envelope from app-installer, create-app and create-plugin (#471)
* fix(app-installer)!: print the application CLI's --json envelope

app-installer's doc comment promised a script that reads `pnpm nocobase ... --json` could read its output the same way, but two things differed: a failure's status was `error` rather than `failure`, and a suggestion's `run` was a shell string rather than `{ command, args }`. Both now match, including the Node.js check in bin/run.js. Suggestions that chained two commands with `&&` are split into two, and text output still prints each command as one quoted line.

`status` no longer prints `undefined` for an advice step that has no command.

tests/envelope.test.ts pins every member of the envelope, and tests/scripts/json-envelope-parity.test.mjs builds the same outcomes through both CLIs and compares the printed documents, so the two cannot drift silently again.

BREAKING CHANGE: scripts parsing app-installer --json must read `status: "failure"` and `run: { command, args }`.

* feat!: print the application CLI's --json envelope from create-app and create-plugin

create-app and create-plugin each had a JSON result of their own. create-app printed a flat object with `status: "error"` and no error codes; create-plugin named itself `operation`, kept its plan at the top level, gave suggestions as strings, and wrote failures to stderr. Both now print `{ schemaVersion, ok, command, status, result | error, warnings }` on stdout, like app-installer and every `pnpm nocobase` command.

create-app reports one code per stage a failure stops at (INVALID_USAGE, TEMPLATE_DOWNLOAD_FAILED, SCAFFOLD_FAILED, INSTALL_FAILED, DRIVER_VERIFICATION_FAILED), with the stage, directory and progress under error.details. create-plugin answers --dry-run with success-noop. Both answer an unsupported Node.js with the envelope under --json. Exit codes are unchanged.

app-installer and scripts/smoke-create-app.sh read create-app's new result and still read the flat one an earlier release prints. The parity test now covers all three standalone tools, and AGENTS.md records the rule.

BREAKING CHANGE: scripts parsing create-app or create-plugin --json read results under `result`, failures under `error` with `status: "failure"`, and create-plugin failures on stdout.

* fix(create-app,create-plugin)!: flush stdout before exiting and name the project in the install retry

The `--json` document was written and `process.exit` called in the same tick; on a platform where a piped stdout is asynchronous the document could be cut short, which the Node.js guard had already worked around on its own path. Both `bin/run.js` now exit through one helper that waits for stdout and stderr to drain, as app-installer's does.

create-app's INSTALL_FAILED suggestion ran `pnpm install` with no directory, so a caller executing `run` as given ran it where create-app was invoked rather than in the project; it now names the project with `--dir`. create-plugin's Node.js guard normalizes the version the way the other two guards do, and create-app pins its one-line output, which app-installer's `parseCreateResult` reads by line.

---------

Co-authored-by: li'mac <limac@Lis-MacBook-Air.local>
2026-09-27 19:21:46 -07:00
84cc7d2c3b feat(hub-cli)!: move Hub publishing out of app-cli into @nocobase/hub-cli (#470)
* feat(hub-cli)!: move Hub publishing out of app-cli into @nocobase/hub-cli

`release upload` and `release deploy` leave @nocobase/app-cli and
become `hub upload` and `hub deploy` in the new @nocobase/hub-cli
package. The `nocobase.cli.publishing` flag is removed: an application
gets the commands by depending on the package, and the Default template
declares it in devDependencies.

`hub deploy` uploads the archive and deploys it; with `--release-id` it
deploys a Release already on the Hub. `hub upload` only uploads.

app-cli gains a general mechanism for this. A direct @nocobase/
dependency whose package.json names a CLI entry in
`nocobase.cli.entry` contributes that entry's defineCliPlugin commands
without an entry in cli/plugins.ts, and a package that is not an
application plugin takes its topic from its name without the `-cli`
suffix. Finding such packages reads JSON only; a run imports one only
for a command under its topic, whole-tree help and `commands`, and a
declared package nobody installed is reported as PACKAGE_NOT_INSTALLED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(app-cli): accept colon-joined ids for CLI packages and check the entry's topic

The package a run imports was chosen from the first space-separated word of the command line, so `hub:deploy`, which oclif accepts as `hub deploy`, matched no topic and the package was never loaded; the same split kept a colon-joined id off the PACKAGE_NOT_INSTALLED path. Both now split on a space or a colon.

A loaded entry is also checked for the topic its package name gives, not only its package name, so an export that mounts under another topic is rejected rather than assembled under one the run did not select. The entries of several packages are imported concurrently, with the first failure in package order reported. The PACKAGE_NOT_INSTALLED message no longer asserts that an uninstalled package provides commands, since that cannot be known without its manifest, and `hub deploy --help` says the default key with --release-id includes the --config content, as the README and the code already did.

---------

Co-authored-by: li'mac <limac@Lis-MacBook-Air.local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:19:46 -07:00
Katherine 16d1158bd0 docs(deployment): add a section on handing a deployment to an AI Agent (#458)
* docs(deployment): add a section on handing a deployment to an AI Agent

Keep the existing deployment pages and add one section to the index that
says where to open the session for each deployment mode, which Skill
drives it, what to state in the conversation, and what the report must
contain. Note that a Hub run from source on the build machine is built
without --target, that HUB_URL carries Hub's own mount path, and point
the Default template README at the Hub publishing page on GitHub, since
the docs site does not serve that path.

* docs(deployment): cover the plain Node.js route in the AI Agent section

Name the standalone route as Node.js or Docker so it matches the mode
table above, link both pages, and name the deployment Skill an
application ships.
2026-09-28 09:17:56 +08:00
chenos 46ce11fabb refactor!: read client runtime values from the server-rendered config and build path-free clients (#455)
* refactor(app-client): read the mount path from the server-rendered config

resolveAppBase and resolveAppUrl now read app.basePath from the client
configuration the server renders into the page, and throw when the page
carries none, instead of reading window.APP_BASE_PATH or falling back to
the bundler's base. The router basename comes from the same value, so
AppRuntimeDefinition.basename is removed and the templates no longer take
it from the deprecated portal SDK.

The AI employee plugin no longer builds an API client when its modules
load. Its service functions take the ApiClient as their first, required
argument, and pages use the new useAIEmployeeClient() hook, which binds
them to the application's client. The registry's NocoBaseAIService now
requires its client, and the nocobaseAIService singleton is gone.

The templates render a config block in their test setup, as the server
would, and drop the unused assetUrl helper.

* refactor(app-server): stop putting runtime values on window

The SPA routes render only the lang attribute and the client
configuration block. window.APP_BASE_PATH, window.NOCOBASE_API_URL and
the window.__nocobase_api_client_* storage settings are no longer
injected: the client reads the mount path and API URL from the
configuration, and nothing read the storage settings except the portal
SDK's unused AuthSession. SpaConfig.runtime, createNocoBaseSpaRuntimeGlobals,
injectSpaRuntimeGlobals and the SpaRuntimeGlobals types are removed, and
runtime-globals.ts becomes runtime-html.ts.

The templates drop the matching spa.runtime defaults and the Vite
defines that baked NOCOBASE_API_URL, NOCOBASE_AUTHENTICATOR and the
NOCOBASE_WS_* values into the client; pnpm dev no longer sets
NOCOBASE_API_URL for its child processes.

* refactor!: remove the portal SDK and rename the portal presets

@nocobase/app-portal-sdk is deleted. Its runtime helpers were already
deprecated, and after the client stopped reading window globals the
templates used nothing from it; its client and routing entry points had
no users in this repository. The peer-dependency list, the registry
dependency check, the root lint configuration and AGENTS.md drop it.

The dev-config presets that still carried the Portal name are renamed:
vite/portal with createPortalViteConfig becomes vite/app with
createAppViteConfig, and the ESLint createPortalConfig becomes
createApplicationConfig. Browser code is now linted against reading
import.meta.env beyond PROD, DEV and MODE.

The templates no longer define __PORTAL_TEMPLATE_NAME__,
__PORTAL_TEMPLATE_VERSION__ or the unread __PORTAL_DEV_SOURCE_ROOT__ at
build time. The server publishes the application's displayName and
version from its package.json as app.displayName and app.version in the
public client configuration, and the sidebar footer reads them there.
The bootstrap loading classes become app-bootstrap-*.

* feat!: build path-free clients and choose the mount path at run time

The shared Vite preset builds with a relative base, so every chunk,
preload dependency and asset resolves against the module that references
it and one build can be mounted at any path. renderBuiltUrl and its
window.APP_BASE_PATH expression are gone. The development server still
needs an absolute base and takes it from APP_BASE_PATH, which pnpm dev
now resolves once and passes to both children; it refuses to start
without one. The templates no longer set a base.

The server points the remaining ./assets/ URLs in index.html, meta
content included, at the path it is mounted at when it renders the page.

pnpm build records nocobase.relocatable instead of nocobase.basePath and
no longer copies APP_BASE_PATH into dist/.env, so the deployment chooses
the mount path. DEFAULT_APP_BASE_PATH in @nocobase/app-server/support is
the only place /main is spelled.

In PROXY_TARGET_URL mode no local server renders the page, so a
development plugin from @nocobase/app-cli/dev/proxy takes the client
configuration from the remote page and substitutes the local mount path
and API URL.

* feat!: choose the mount path when installing or running a build

app-installer takes --base-path and keeps the mount path in app.env, so one
relocatable archive installs anywhere and moves by editing APP_BASE_PATH.
An archive from before relocatable builds records the path its client was
compiled for and is only installed, upgraded to or rolled back to there.

The Hub refuses such an archive unless it was built for /<appId>. The
template Dockerfiles no longer take APP_BASE_PATH as a build argument; the
image defaults to /main (/hub for the Hub) and -e APP_BASE_PATH moves it.
The deployment Skill and documentation drop the build-time mount path.

* chore: add changesets and upgrade steps for the client runtime configuration

The removed @nocobase/app-portal-sdk is dropped from the consumed changesets
in .changeset/pre so the next prerelease run can still read them, and the
one that named only it is deleted. The nocobase-app-upgrade Skill's edge
cases gain the steps an existing application takes to move to the
server-rendered client configuration and relocatable builds.

* ci(smoke): give the deployed archive its mount path at run time

A build no longer copies APP_BASE_PATH into dist/.env, so the extracted
archive started at the server's /main default. The Hub serves at /hub, and
the smoke waited there until it timed out. Pass the path the checkout
served, as a deployment does.

* fix: resolve review findings on client runtime configuration

- app-server rewrites every `./` URL in `index.html` to the mount path,
  not only `./assets/`: a relative base writes every `public/` file the
  page names as `./<file>`, and a deep route resolved those elsewhere.
- app-installer keeps `/hub` for a Hub archive installed without
  `--base-path`; a build records no path and ships no `.env`, so the
  server's `/main` default moved it. Adds a test for `--base-path /`.
- The proxy-mode client configuration fetch reports the status it met
  and a redirect to another origin instead of blaming the URL.
- `createDevProxy` and `createDevClientConfigPlugin` share the local
  prefix computation; the JSON escaping stays local, with the reason.
- The Vite preset README says why serve-mode resolution outside
  `pnpm dev` needs `APP_BASE_PATH` and how a build is previewed.
2026-09-27 06:52:43 -07:00
Jiannx 4b086fa526 docs(mail): mail plugin (#125)
* feat(mail): add mailbox sending and sync architecture

* feat(mail): add Gmail and Microsoft providers

* feat(mail): add settings and development pages

* fix(mail): harden sending and mailbox synchronization

* feat(mail): add mailbox workspace and conversation view

* fix(mail): invalidate mailbox requests on account refresh

* fix(mail): stabilize mailbox view state

* refactor(mail): consolidate workspace under dev route

* refactor(mail): keep all UI under dev route

* feat(mail): add account management and operation logs

* feat(mail): add reply, forward, and scheduled delivery

* feat(mail): add account management actions

* feat(mail): complete workspace and provider features

* feat(mail): complete core management experience

* fix(mail): harden provider and permission boundaries

* fix(mail): harden persistence model

* feat(mail): add credential-based IMAP/SMTP provider MVP

* feat(mail): improve account and workspace management

* feat(mail): enhance mailbox management and sync flows

* feat(mail): harden mail flows and account selection

* feat(mail): complete mail workspace capabilities

* test(mail): align mail plugin test coverage

* feat(mail): bundle built-in providers and modernize workspace

* fix(mail): address provider sync and draft review issues

* feat(mail): improve composing, delivery logs and mailbox sync

* fix(mail): improve pagination and delivery safety

* feat(mail): add resumable sync and consolidate migrations

Recover mailbox synchronization, retry incomplete content, reconcile sent messages and drafts, and improve account switching and runtime logging.

Consolidate unreleased migrations and changesets, expand regression coverage, and update Mail documentation and integration guidance.

* refactor(mail): separate contracts and runtime responsibilities

* fix(mail): harden persistence, drafts and workspace refresh

* refactor(mail): remove implementation migrated to Pro

* docs(mail): reorganize guides around AI-assisted setup and usage

* docs(mail): clarify provider setup and access boundaries

* docs(mail): focus guides on agent-driven use cases
2026-09-27 21:14:17 +08:00
chenos a4ee8aa542 feat(app-installer): generalise hub-installer into app-installer for deployment archives (#451)
* feat(app-server): keep standalone storage under APP_STORAGE_DIR and record build metadata

A standalone application reads APP_STORAGE_DIR for its storage directory, so a
deployment whose code root is replaced per release keeps one storage directory.
The Hub template still honours HUB_STORAGE_DIR when APP_STORAGE_DIR is unset.

pnpm build records nocobase.basePath and nocobase.builtAt in dist/package.json,
and the templates' ecosystem.config.js derives the pm2 process name from the
application's package name, overridable with APP_PM2_NAME.

* refactor(app-installer): rename @nocobase/hub-installer to @nocobase/app-installer

A pure rename ahead of generalising the installer beyond the Hub: the package
directory, name and bin, its unreleased changesets, the smoke script and its
workflow, and pnpm unreleased:hub-smoke, now unreleased:installer-smoke. The
package was never published, so nothing outside this repository refers to the
old name. Behaviour is unchanged.

* refactor(app-installer): generalise the installer's layout, state and backups

Releases are named by version and build time (releases/<version>_<time>/app),
so two builds of one version are two releases; upgrade --rebuild becomes an
ordinary upgrade to a fresh build, which removes the staging and swap logic.
installer.json records the application name, base path and source. The runtime
variables move to app.env with APP_STORAGE_DIR (and HUB_STORAGE_DIR for Hub
releases published before it), health and URLs follow the release's base path,
and backups cover every SQLite database config.yml declares. Messages name the
application rather than the Hub, and the pm2 name defaults to the directory.

* feat(app-installer): install and upgrade from a deployment archive

install takes --archive <dist.tar.gz> or --template hub[@version], one of them
and only one. An archive is unpacked with the tar library pnpm build packs it
with, so a server needs no tar and no pnpm; its manifest names the release, and
an archive without a base path and build time, built for another machine, or
lacking the dialect's driver is refused with the build command that fixes it.
upgrade --archive requires the same application name and base path and refuses
an older version; a release writing its data inside itself is refused as well.
An upgrade takes the kind of source the install did.

* feat(app-installer): source-aware status and rollback advice, free-port suggestion

status and rollback tell an archive installation to build its archive again
for this machine's Node and upgrade to it, where a template installation gets
upgrade --rebuild; status reports the source and skips the registry for an
archive installation. install names a free port when the chosen one is taken.
The README documents both sources, release ids, app.env and every error code.

* test(app-installer): smoke both sources in CI and unreleased:installer-smoke

The smoke script takes --source template or --source archive. The archive run
installs a deployment archive, upgrades to a later build of the same version,
rolls back, restores the database after a migrating upgrade, and rolls back
automatically from a build that fails to start. CI gains a job that builds the
default template's archive from the checkout and runs it; unreleased:installer-
smoke creates and builds an application from the snapshot and runs both.

install reports the release's build time. The template alignment test no
longer expects each template to name its own pm2 process.

* docs: document app-installer and rename its global Skill

The global nocobase-hub-installer Skill becomes nocobase-app-installer and
covers both sources: building and installing a deployment archive, and the Hub
template. The nocobase-deployment Skill offers app-installer for a standalone
server, and the documentation site gains a page for it, with the Hub page,
the deployment overview, operations and standalone pages updated. Examples that
run published Hub images keep HUB_STORAGE_DIR, which those images still read.

The unreleased installer changesets, written for the Hub-only package, are
replaced by one describing the package as it now ships.

* docs: name only APP_STORAGE_DIR in the deployment pages

The Hub pages, operations page and the app-development Skill no longer
document HUB_STORAGE_DIR: nothing has been released under the old name that
the documentation needs to keep serving.

* refactor: drop HUB_STORAGE_DIR in favour of APP_STORAGE_DIR

The Hub template no longer reads HUB_STORAGE_DIR, which removes its own path
policy: the runtime applies APP_STORAGE_DIR for every standalone application.
app-installer stops writing the old name for a Hub installation. Nothing has
been released under the old name that has to keep working; a deployment that
sets only HUB_STORAGE_DIR renames it before upgrading.

* fix(app-installer): write HUB_STORAGE_DIR for a Hub until one reads APP_STORAGE_DIR

Every Hub release published so far reads only HUB_STORAGE_DIR, so a template
install of one kept its data inside the release and was refused with
STORAGE_IN_RELEASE. The installer writes the old name beside APP_STORAGE_DIR
for a Hub installation again; a Hub that reads APP_STORAGE_DIR ignores it.
Remove it once such a release is published.

* fix(app-installer): address review findings on PR 451

Recognise a Hub by its manifest's nocobase.templateKind rather than by the
install source, so a Hub project deployed from an archive keeps its App Host
child (treekill off) and its confirmations name the hosted applications;
installer.json records the kind. Suggestions no longer carry a placeholder in
a command meant to run as printed, and every rebuild suggestion made during an
upgrade builds for the installed base path. An unreadable config.yml stops an
upgrade with CONFIG_UNREADABLE instead of passing as "no databases".
rollback --to <version> resolves to the newest build of that version other
than the running one. The upgrade prechecks are hoisted out of both source
branches, and tests cover the storage check rolling an upgrade back and an
unrecorded release directory being adopted.

* docs(skills): route Hub installs and app-installer deployments to nocobase-app-installer

nocobase-create-app is for installing, creating or trying NocoBase, and hands a
request to install a Hub, or to deploy an application with app-installer, to
nocobase-app-installer. Only a Hub whose own code will change is created as a
project. Both Skills now carry the same routing table, and their descriptions
name what each is for and what it hands over.
2026-09-26 20:42:41 -07:00
chenos 19a8431c10 docs(skills): add nocobase-hub-installer and document the Hub installer (#447)
* feat(hub-installer): install a Hub from the published template

Add @nocobase/hub-installer for servers that run an unmodified Hub.

`install <dir>` resolves the template version, generates and builds the
Hub in `.build/<version>/` with create-app, unpacks only the deployment
archive into `releases/<version>/hub`, writes `hub.env` and `config.yml`
outside the release, applies the migrations with the release's own CLI,
switches `current`, and starts the Hub with pm2 until `/hub/api/healthz`
answers. A failure before the switch removes everything it wrote.

`status` reports the current version, health, pm2 process, releases,
Node compatibility and whether the registry has a newer version.

The pm2 configuration runs `node` itself with the entry file as its
argument, because pm2's fork mode loads a script through a wrapper where
`import.meta.main` is false and the Hub never starts.

A smoke test installs the published template on Debian bookworm with
Node 24 on pull requests that touch the package, and daily, since the
template ships no lockfile and a new dependency release can break it.

* feat(hub-installer): upgrade and roll back a Hub

`upgrade` builds the new release beside the running one, or reuses a
release already on disk, and checks it with its own `config check` and
`db apply --dry-run` before any downtime. It then stops the Hub, copies
the SQLite database, config.yml and hub.env to backups/, switches
`current`, migrates and starts the new release behind a health check.
A failure after the switch rolls back automatically, restoring the
database when the new release may have migrated it: exit 3 when the
previous release is healthy again, 4 when it is not. Releases beyond
`--keep` are pruned.

`rollback` returns to the release the last upgrade came from and
restores the pre-upgrade database only when that upgrade migrated.

An operation interrupted during the downtime is recorded in
installer.json: status reports it, upgrade refuses to stack on it, and
rollback recovers by undoing an interrupted upgrade or finishing an
interrupted rollback. Both commands confirm first or take `--yes`, and
a non-SQLite database requires `--backup-done`.

The smoke test now upgrades from a copy of the installed release and
rolls back, exercising the downtime path without a second build.

* feat(scripts): add unreleased:hub-smoke for the Hub installer

`pnpm unreleased:hub-smoke` installs a Hub with the snapshot's
hub-installer from the local registry, checks it, upgrades from a copy
registered as an older release, rolls back and checks again. It runs
pm2 under a short temporary PM2_HOME, stops it afterwards, and keeps
the Hub and its logs outside the repository.

The CI smoke test can only see changes to the installer itself: it
installs the published template. This covers changes to the Hub
template, app-cli, app-host and create-app before they are released.

Both run scripts/smoke-hub-installer.mjs, which replaces the inline
checks the workflow carried, so the local and CI checks cannot drift.

* fix(hub-installer): address the install review

- Refuse a pm2 name that is already registered: `pm2 start` on a taken
  name restarts that process with this install's environment instead of
  starting a new one, and START_FAILED then deleted it.
- Check before the first write that the port binds, that every
  `--set-from-env` variable is set, and bound `--port` and
  `--health-timeout`; a regular file at the target is a precheck error.
- SIGINT/SIGTERM stop the running step and let the failure handling
  clean up; a second signal exits. Children run in their own process
  group, so a timeout or interrupt stops what pnpm started, and the
  promise settles only once the group has exited.
- Write the pm2 config and installer.json before switching `current`,
  so the switch is the last write before starting; remove parents the
  install created; keep `.build/` on failure under `--keep-source`.
- Fail fast when pm2 reports the new process as crashed, instead of
  waiting out the health timeout.
- `status` no longer starts the pm2 daemon; the lock is linked into
  place with its pid already written; dist-tags use `hasOwn`;
  `install --help` prints help; `config set` values are masked in
  output; an unsupported Node exits 2.
- The smoke test builds the package and runs dist/, and also runs on
  lockfile and workspace changes.
- Tests drive `install()` through a fake of every child process it
  starts, and runCommand through real process groups.

* docs(skills): add nocobase-hub-installer and document the Hub installer

Add the global `nocobase-hub-installer` Skill for installing, upgrading,
rolling back and checking an unmodified Hub with @nocobase/hub-installer.
It routes a Hub whose source changes to nocobase-create-app with
`--template=hub` and Docker to the documentation, runs every command
through `npx --registry=${NOCOBASE_REGISTRY:-https://npm.nocobase.ai}`
with `--json`, and makes the agent relay the confirmation notes before
passing `--yes`.

NocoBase 3 publishes to https://npm.nocobase.ai, not to the public npm,
where @nocobase/create-app answers 404. nocobase-create-app and the
agent guide on the documentation site claimed the opposite, so on a
machine without a scoped registry `pnpm create @nocobase/app` failed.
They now name the registry before `create`, in the same
NOCOBASE_REGISTRY-aware form, which unreleased:env still redirects.

skills/README.md covers both global Skills; the Hub deployment pages
gain an installer route, and nocobase-deployment tells a Hub project
apart from an unmodified Hub.

* fix(hub-installer): address the upgrade and rollback review

rollback:
- Restore only from a backup that holds a database. An external
  database is never in one, so a Hub on PostgreSQL or MySQL used to
  stop, fail to restore, and stay down.
- Keep installer.json in step with the `current` link: `current` is
  written only after the link moves. A failed stop changes nothing and
  exits 1; a target that does not start stays pending, and running
  rollback again retries it, restore included.
- Recover an interrupted upgrade by what it actually did: before the
  switch nothing was migrated, so the old release is just started; an
  interrupted rollback keeps its backup and restores it again.

upgrade:
- Record the backup in `pending` only once it is complete, and mark the
  switch before the link moves; remove a half-written backup on abort.
- Move the switch inside the rollback's error handling.
- Write the successful result before pruning, then prune.
- Never prune the release upgraded from; `--keep` is at least 2.
- Keep the new release, and the pending state, when the automatic
  rollback fails too, so `rollback` can finish or return to it.
- Refuse a downgrade; reuse a recorded release only when it was built
  for this machine, otherwise rebuild it and replace its record.
- Check that the pm2 process belongs to this root and that the port is
  free once the Hub is stopped; wait for health failing fast on a
  crashed process; read the state under the lock; UTC backup names.

Flow tests drive both commands through the fake world: exit 3 and 4,
recovery after each, external databases, stop failures, interrupted
upgrades, rebuilds, ownership and pruning. The smoke test pins the
upgrade target to the installed version.

* docs(skills): sharpen nocobase-hub-installer from agent trials

Two trial runs of an agent following the Skill against the unreleased
snapshot surfaced gaps: reading config.yml to report the first sign-in
contradicted keeping it out of the conversation; nothing covered
reaching the Hub without a proxy (--host 0.0.0.0), changing the origin
after installing, relaying result.nextCommands, result.notes and
result.pruned, pruning under --keep, or npx prompting in a terminal.
Each is now covered, and the pm2 check no longer starts its daemon.

* test(scripts): cover the restore and automatic rollback paths in the Hub smoke test

The smoke test upgraded to a copy of the same release, which applies no
migrations, so the rollback never restored the database and no upgrade
ever failed. It now also rolls back an upgrade recorded as migrating and
checks that a table written after it is gone, and upgrades to a release
whose server entry throws, expecting exit 3 with UPGRADE_ROLLED_BACK and
the previous release healthy again.

Both scripts refuse port 13010, where the Hub's App Host listens; a Hub
on it failed its health check with no clear reason.

* feat(hub-installer): start through a launcher, report endpoints, and make suggestions runnable

pm2 now runs launcher.mjs in the Hub root. On every start it reads
hub.env and resolves `current`, then becomes the Hub through
process.execve, keeping the pid pm2 watches and passing node flags on.
A plain `pm2 restart` therefore applies an edited hub.env or a switched
release; ecosystem.config.cjs no longer captures either.

- status reports `endpoints`: the public URL and origin, and the host
  and port the Hub listens on.
- install reports `initialAdmin` (username, email, and whether the
  password is still the template default, never the password), and
  takes --dir like the other commands.
- Every suggested command and --help example runs as printed: npx with
  the registry named, pinned to this installer's version, paths quoted.
  Suggestions that were not commands, and the release CLI's own
  commands, become messages.
- pm2 4.3 or later is required, the first release that reads
  ecosystem.config.cjs as a configuration.
- Under --json, an unsupported Node.js prints the error envelope.
- The README documents every error code, changing the origin or port,
  and the launcher.

The smoke test switches releases with `pm2 restart` and checks that the
pm2 process runs the expected release's server entry.

* docs(hub-installer): restart with pm2 after editing hub.env, and report the sign-in account

The launcher reads hub.env on every start, so the Skill and the Hub
deployment docs change the origin or port with `pm2 restart` rather than
delete and start. The Skill reports the first administrator from
`result.initialAdmin`, the Hub URL from `endpoints` in status, asks for
a long timeout on install and upgrade, and names pm2 4.3 as the floor.

* docs(hub-installer): address the Skill and deployment docs review

The Skill said an upgrade rebuilds a release after a Node major change,
but an upgrade to the installed version is a no-op; it now says only a
newer version rebuilds and that the installer cannot rebuild otherwise.
Its Docker route links the Compose walkthrough that exists, on the
Chinese page. It names every key of error.details, scopes the loopback
health check to 0.0.0.0, says a SIGKILL leaves the target for the user
to empty, notes that an external database still gets its configuration
backed up, adds that a new port must be free, and gives the --keep
sentence its own step instead of a manual line break.

The Skills README names the pm2 4.3 floor. The English page qualifies
the upgrade backup as SQLite; the Chinese page lists installer.json,
ecosystem.config.cjs and launcher.mjs in the layout and offers the
installer alongside the template when no image exists yet.

* docs(hub-installer): point the Skill and the deployment pages at upgrade --rebuild

The installer can now rebuild the installed version for a machine whose
Node major changed. The Skill tells the agent to run the command the
status warning names, confirmed and read like an upgrade, and that an
interrupted rebuild is finished by running it again rather than by
rollback. Both deployment pages mention the flag next to upgrade.
2026-09-26 17:29:27 -07:00
chenos 601883e59a fix(templates): start the application under pm2 (#443)
* fix(templates): start the application under pm2

pm2's fork mode loads `script` through its own ProcessContainerFork
wrapper, so `import.meta.main` is false in `dist/server/standalone.js`
and `startServer()` is never called. pm2 keeps reporting the process as
online while nothing listens; the Hub template exits and restarts in a
loop with empty logs.

Have pm2 run `node ./dist/server/standalone.js` directly with
`interpreter: 'none'` in all three templates, and explain why in the
pm2 section of the standalone deployment docs.

* fix(templates): resolve the pm2 entry against the config file's directory

`args` was resolved against the directory `pm2 start` ran in, so the
file had to be started from the deployment root. `cwd: import.meta.dirname`
pins it to the file's own directory; `pm2 start /path/to/ecosystem.config.js`
now works from anywhere, both as .js beside an ESM package.json and as
.mjs in a deployment root without one.
2026-09-26 07:15:21 -07:00
chenos ec92b20eda feat(app-cli)!: AppCommand authoring API and one --json envelope for every command (#441)
* feat(app-cli)!: add the AppCommand authoring API on the root entry

AppCommand exposes rootDir, withApp(({ app, env }) => ...), setStatus and
appPath() flags; --json prints one envelope for success and failure, and
CommandError carries a code, suggestions and details. The authoring API moves
from ./plugins to the package root, a ./testing entry provides
bindAppCommand, the runner closes runtimes a command left open, and console
logs of an application a command creates go to stderr.

* feat(app-cli)!: move every command to the AppCommand output contract

Built-in, plugin and generated commands extend AppCommand, return their
result, and throw CommandError on failure, so --json prints one envelope for
every command. Plugin commands reach the application through withApp() and
take appPath() flags; the CLI example's artifact build is a devCommand.

The testing entry exports runAppCommand next to bindAppCommand, dev-config's
ESLint presets refuse the patterns the contract forbids in cli/, app-cli is
registered as identity-sensitive, and the application and plugin-development
Skills each carry the command-authoring guide, kept identical by a test.

* feat(app-cli): keep command diagnostics on stderr and behind NOCOBASE_CLI_DEBUG

An AppCommand's logToStderr keeps writing under --json: stderr never carries
the document, so progress and confirmations no longer disappear for the agent
that asked for JSON. A CommandError keeps its cause off the chain oclif prints,
and NOCOBASE_CLI_DEBUG prints the redacted error chain behind any command's
failure, from source or dist, replacing release's own handling. The Skills
describe using the application's logger inside withApp() for records that
belong in its log files.

* feat(app-cli): make commands discoverable, previewable and recoverable for agents

- `nocobase commands --json` lists every registered command with its source,
  arguments, flags and whether it takes --json, --dry-run and --force; a
  deployment lists no development command.
- Invalid usage suggests the flag or command that was probably meant and the
  command's --help, drops oclif's trailing hint from the message, and always
  exits 2.
- db apply, rollback, redo and reset take --dry-run and return the plan;
  FORCE_REQUIRED carries that plan and the --force/--dry-run forms; a busy task
  lock fails with DATABASE_LOCKED, backed by TaskLockBusyError in
  @nocobase/db. Help examples lead with --dry-run.
- release reports upload and deployment progress on stderr while it waits.
- The runner no longer exits 13 with an unsettled top-level await when a
  reader such as `| head` closes stdout early.

* fix(app-cli): address review findings on the command contract

- withApp() and the db commands report a failure to put the application away as a warning instead of failing work that already succeeded or replacing the failure it ended in; a rerun of `db rollback` no longer rolls back a second batch
- suggestions and config init's next steps name `node <dist>/cli/index.js` in a built dist/, which has no pnpm; AppCommand gains `this.cliCommand(args)` for the same in a command's own suggestions
- usage errors never repeat a typed value, list allowed values and the flags a conflict involves, and release upload/deploy report them as INVALID_USAGE like every other command; `this.error()` reports COMMAND_FAILED rather than invalid usage
- NOCOBASE_CLI_DEBUG prints a failure's diagnostics once, and oclif's raw stack no longer replaces the message and its suggestions
- appPath() defaults declared in baseFlags resolve against the application root
- every --dry-run answers success-noop, and so does a db command that found nothing to do
- a dry run creates no SQLite file that does not exist yet: drivers may report hasStorage(), and a dry run then answers for an empty database without connecting
- the runner's safety net destroys a leaked runtime's scope even when shutdown fails
- a command bound without an id is named after its class, and the Skills' examples name app commands as the runner does

* fix(app-cli): address the remaining review threads on the command contract

- a single misspelled topic, such as `plugn`, now suggests that topic's help instead of only the full command list
- `commands --json` marks an appPath() default with `defaultRelativeTo: "application-root"`, matching what --help says
- the reason oclif gives for a `combinable` conflict is repeated instead of collapsing to a generic message: the pattern expected "Only of the following" where oclif writes "Only the following"
- the runner's own EPIPE handler is removed: it never installed, because @oclif/core swallows EPIPE on stdout at import, and exiting there would have skipped withApp() and the runner's cleanup; a test now covers a command that keeps printing after its reader leaves

* test(ai-employee): give the Skills tests' first render room to load the page

The first render in the file waits for the employee heading with findBy's default 1 s, which the page's import graph can outlast when CI runs every package's tests at once. The Tools tests hit the same wait and got a 10 s timeout in #413; the Skills tests now match.

* test(app-cli): inspect a workspace of the test's own rather than the repository

The workspace-root inspection test from #439 scanned the real repository's packages/, where other app-cli tests create and delete temporary applications under tests/.tmp while they run. Run together, the scan read one mid-write and the inspection failed. It now builds the one-application workspace it needs.
2026-09-25 19:57:00 -07:00
chenos dbf5631e1f fix: clear the leftovers of the CLI consolidation (#439)
* chore: replace the command ids left over from the CLI consolidation

The scheduler and template-print documentation still named `schedule sync`
and `skills:sync`, the template reference pages showed `pnpm migrate`, and the
global create-app Skill taught `pnpm config:*` scripts that applications no
longer carry. The create-app smoke test now records `<topic> <command>` ids
directly instead of mapping them back to the old script names.

* chore: drop references to the removed client:inspect and server:inspect

Application inspection was removed in #329, but the plugin-development
Skill, skills/README.md and a few comments still sent readers to
`client:inspect` and `server:inspect`. `plugin inspect` covers registration
state only, so the guidance now points there for registration and drops the
composition diagnostics. The authentication test that asserts no command is
prescribed now checks for `config init` rather than the old script name.

* fix: clear the remaining leftovers of the CLI consolidation

A second sweep after #431, #434, #437 and #438 found guidance and code that
still described the old command line.

- `plugin inspect` suggested the removed `pnpm plugin:register` and
  `pnpm skills:sync` scripts; it now suggests `pnpm nocobase plugin register`
  and `skills sync` with the flags that located the App, and the remaining
  colon-form hints name the real commands.
- The Default template never registered the scheduler's CLI entry, so the
  documented `pnpm nocobase scheduler sync` did not exist there.
- `@refinedev/cli` and `tsc-alias` are spawned by `nocobase build` and are now
  declared as optional peers of `@nocobase/app-cli`.
- `peers:check` covers `@nocobase/app-cli` again after its move to
  `packages/app`, and `deps:check` scans plugin `cli/` sources and source
  directories named `dist`.
- Skills, templates, plugin AGENTS.md copies, READMEs and the docs site now
  describe current commands, the `metadata/` directory from #437, the Hub
  archive path, `release` availability and the two `dist/package.json`
  scripts. Examples and Hub ignore `/.env`.

* fix: address review threads on the CLI leftovers

- the templates' Docker build contexts exclude build output where builds write it (each workspace package's dist/, the documentation sites') instead of every directory named dist/, so a source directory with that name, such as app-cli's dist commands, needs no exception
- `plugin inspect` suggests `skills sync --package <full name>` rather than the compatibility `--plugin`, and a test covers suggestions for a `--workspace-root`/`--app` inspection
- the CLI example Skill states when two plugin topics collide: once the scope and an `app-plugin-` prefix are removed
- `peers:check` fails on a manifest that does not parse instead of leaving the package out
- the smoke test's fake pnpm records a top-level command such as `nocobase build` by its own id, reading the top-level commands from app-cli
- the plugins changeset no longer lists create-app, whose change another changeset already covers
2026-09-25 19:34:23 -07:00
chenos ae43f41a94 refactor(app-cli): tidy the release commands and print --json failures on stdout (#434)
* refactor(app-cli): tidy the release commands and print --json failures on stdout

- Name the JSON operation release:upload and release:deploy, after the
  command ids, instead of app.upload and app.deploy.
- Register the release commands only in a source checkout: a built dist/
  has no archive for release upload to send.
- Resolve --file and --config from the current directory; the default
  archive stays under the App root.
- Name the missing, invalid or unknown flag in an argument error without
  repeating any value, and print an unexpected failure's cause to stderr
  under NOCOBASE_CLI_DEBUG.
- Give both commands a description and examples, and share their run
  logic in one ReleaseCommand base.
- Print --json failures of the plugin, package and skills commands on
  stdout, as every other command already did.
- Move the Hub publishing guidance into the nocobase-deployment Skill and
  describe the stdout-only --json contract in the CLI reference.

* test(app-plugin-hub): pass the publishing config as an absolute path

publishToHub now resolves --config from the current directory rather
than the App root, so the fixture names the file it wrote.

* docs(app-plugin-hub): resolve release --config and --file from the current directory
2026-09-25 02:13:32 -07:00
chenos 02d54029a8 refactor(app-cli)!: merge nb3-cli and app-tools into one application command line (#431)
* refactor(templates): drop the forwarding files in cli/

The templates re-exported @nocobase/app-cli through cli/database-command.ts,
cli/hub-publishing.ts and cli/commands/i18n-check.ts, and passed
createAppCommands through cli/standard-commands.ts. cli/commands/index.ts now
calls createAppCommands itself, the shared i18n check test lives in app-cli,
and app-plugin-hub imports publishToHub from the package rather than from a
template. app-cli drops the ./database-command and ./commands/i18n-check
subpaths that only those files used.

* refactor(app-cli)!: absorb nb3-cli and app-tools into one command line

@nocobase/app-cli now provides the nocobase bin and carries the command
assembly, the plugin contract and plugin/Skill management from nb3-cli, and
the dev, build, start and server-deps tooling from app-tools. Both packages
are removed.

Commands are found by path under src/commands and an application's
cli/commands, and the runner locates the application from the nearest
package.json (nocobase.templateKind or nocobase.buildTarget) or
NOCOBASE_APP_ROOT. A built-in command is dispatched without importing the
application's plugins. Development commands are not registered in a built
dist/, and the tooling they need is declared as optional peers. build and
dev hand the plugins' hooks to the scripts in NOCOBASE_CLI_HOOKS instead of
asking a second process.

Consumed changesets in .changeset/pre name the removed packages and are
renamed to @nocobase/app-cli, as the app-client consolidation did, so the
next prerelease run can still read them.

* refactor(plugins)!: take the CLI topic from the package name

A plugin's CLI topic is now its package name without the scope and
app-plugin- prefix, so the scheduler's commands move from schedule to
scheduler and the CLI example's from demo to cli-example; the built-in
topics avoid every plugin name. The workflow plugin's check and build are
devCommands, which a built dist/ leaves out.

Plugins and the create-plugin template peer on @nocobase/app-cli instead of
@nocobase/nb3-cli, and the workflow tests and skill-eval harness run the
Default template's CLI through the bin with NOCOBASE_APP_ROOT.

* refactor(templates)!: run every command through the nocobase bin

The templates drop scripts/*.mjs, cli/index.ts and cli/commands/index.ts.
Their scripts are postinstall, dev, build, start and the quality checks;
every other command is pnpm nocobase <topic> <command>, which pnpm resolves
to the bin without a script. The Default template enables the Hub
commands with nocobase.cli.publishing, and all three declare tar for the
build's --tar.

create-app runs pnpm nocobase skills sync and prints the new next steps.
The smoke script and the repository's template tests follow the new layout.

* docs: describe the merged CLI and use the new command ids

AGENTS.md, the application and plugin-development Skills, the docs site,
package READMEs and runtime messages now name nocobase db repair,
config init, locales check, release upload and the other new ids instead
of the removed app-topic ids and script aliases. The upgrade Skill gains the
step-by-step move to the new layout, which the changeset points to.

The global nocobase-create-app Skill and skills/README.md are left for a
follow-up after the release, because they reach users on merge.

* chore: cover the packages whose Skills name the new command ids

* refactor(app-cli)!: name the Collection and build commands by what they act on

db doctor compares Collection metadata with the schema, not migrations, so
it becomes collections doctor beside collections generate, and db holds only
migrations, seeds and locks. server-deps retarget and verify are two steps of
the build, run again on an existing dist/, so they become build retarget and
build verify. A dist topic was considered and rejected: a commands/dist
directory is ignored by the repository's .gitignore, Prettier and ESLint.

Topic and command descriptions now say application rather than app and Skill
rather than skill, package describes the one thing it does, and skills sync
mentions the .claude/skills links it writes.

* refactor(app-cli)!: move retarget and verify to a dist topic as dist retarget and dist check

Both act on an existing dist/ and never build, so they belong to a dist
topic rather than under build, which returns to being a plain command.
verify becomes check, the verb every other checking command uses
(config check, locales check, workflow check).

A dist directory is ignored by the repository's .gitignore, both Prettier
ignore files and the shared ESLint preset, and commands are found by their
directory, so src/commands/dist/ gets an exception in each of the four with a
comment saying why. tests/command-files.test.ts fails if any command file is
ignored by git, Prettier or ESLint, so losing an exception cannot go
unnoticed. The templates' AGENTS.md warns applications away from those
directory names in cli/commands/.

* docs(app-skills): add a command-line reference for the application CLI

nocobase-app-development gains references/cli.md: a task-to-command map, the
agent conventions --help does not state (--json, exit codes, --dry-run, and
that db reset, rollback, redo and repair refuse without --force outside a
terminal, which is not to be worked around), how to read the data-model
snapshot and keep it fresh, how to write an application command, and what a
built dist/ registers. The Skill links it before any pnpm nocobase command;
the deployment Skill notes dist retarget, dist check and running commands in
dist/; the three templates' AGENTS.md keep only the essentials and point to
the reference instead of repeating it.
2026-09-25 01:28:45 -07:00
YANG QIA 757eedfbfe fix(app-client): never fail startup on a page without authz (#433)
A page without authz inherits its nearest ancestor page; a first page defaults to unrestricted-only (protected app and settings pages) or skip (guest, optional and dev pages), with a development warning.

Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE
2026-09-25 13:38:45 +08:00
Zeke ZhangandClaude Opus 5.5 91cc403007 feat(ui-library): publish the page and route overlay components as registry components (#426)
* feat(ui-library): publish the page and route overlay components as registry items

Move PageContainer, PageHeader, RouteDialog, RouteDrawer, RouteChildPage and
useRouteOverlay out of the templates' client/components/ and into the UI
Library as two items, page-ui and route-overlay-ui, so applications and
plugins install one maintained source instead of copying template files.

- ui-library: add the items under registry/page/, conforming to the library's
  authoring rules (.js relative imports, explicit export types, tailwind-merge
  instead of @/lib/utils, prefixed translation key routeOverlay.close with
  shipped locales), preview demos, the dialog primitive, and generic preview
  wiring in website/app.tsx.
- templates: ship byte-identical installed copies under
  client/extensions/nocobase-page-ui/ and nocobase-route-overlay-ui/, import
  from there, and add routeOverlay.close to client/locales/.
- tests/scripts/template-ui-library.test.mjs fails when a template copy drifts
  from the library source.
- Point the app development Skill, the plugin development Skill, the template
  AGENTS.md files, the docs site and the library's own guides at the new
  locations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui-library): publish one component per item, installed into client/components

The page and route overlay components are single business components, not a
complete feature, so they follow shadcn's convention for components rather
than the block layout auth-ui uses.

- Split page-ui and route-overlay-ui into page-container, page-header,
  route-dialog, route-drawer and route-child-page. route-dialog and
  route-drawer both list the route-overlay.tsx and use-route-overlay.ts they
  share, so installing either brings its implementation and the two keep one
  Context.
- Target client/components/<file>, so templates keep their original paths and
  imports; revert the import rewrites across pages, tests, Skills and docs.
- Document the two kinds of item in MAINTAINING.md and USAGE.md: a component
  installs into client/components/, a block such as auth-ui into
  client/extensions/nocobase-<item>/, with the rules on relative imports,
  translations, targets, READMEs, upgrading and removal that follow.
- Components ship no locale files; routeOverlay.close is listed in the group
  README and each overlay item's docs.
- Rework the preview demos per item and the sync test for component targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui-library): merge class names with cn from @/lib/utils

The components now live in client/components beside the templates' other
components, which all merge class names with cn from @/lib/utils. Importing
tailwind-merge directly made them the odd ones out.

- Import cn from @/lib/utils in page-container, route-child-page and
  route-overlay, and list utils in their registryDependencies instead of
  depending on tailwind-merge.
- Allow @/lib/utils as the second @/ import an item may use, map it in
  tsconfig.registry.json, and give the preview a real cn (the one shadcn's
  utils item generates) so the drawer's overriding classes merge there too.
- Document the rule, the plugin rewrite, and keeping the templates'
  lib/utils.ts (it also exports assetUrl) when an item requests utils.
- Teach the sync test that utils resolves to client/lib/utils.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 08:43:05 +08:00
YANG QIA c8ddd7d7f2 refactor(authorization): consolidate the authorization API (#417)
Consolidates the authorization library and plugins around one model: judgement by resource types and built-in composite resources, display through authz.ui, unified rule shapes, and a snapshot that agrees with can(). Existing databases must be reset.

Claude-Session: https://claude.ai/code/session_01KLK7YsekQSSZa8iRjRBmuE
2026-09-24 23:03:12 +08:00
chenos d18e964348 feat(config): declare environment variables on their sections and list them with config:env (#425)
* feat(config): let sections declare their environment variables and drop unread ones

* feat(config): drop server/environment.ts and list environment variables with config:env
2026-09-24 06:07:45 -07:00
DrolandClaude Opus 5.5 c2aceaa7b4 feat(ai-employee)!: give an execution one state, one context, and declared dependencies (#364)
* fix(ai-employee): widen the nocobase-ai Registry plugin constraint

The item's requiresPlugins range was written as >=0.0.1 <0.1.0 when the
Registry was first published and never revisited. Under standard semver
prerelease rules that range excludes the plugin's own 0.1.0-beta.19, and
every other version the 0.1.0 line has shipped, so a consumer checking
the constraint before installing would refuse all of them.

Rebuild public/r from registry.config.json so the published JSON carries
the corrected range.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): document nocobase-ai Registry installation

The App Skill assumed client/extensions/nocobase-ai was already present
and offered no way to obtain it, so an agent working in a generated
application had no correct path to the chat UI and fell back to a hosted
shadcn add whose version can differ from the installed plugin.

Order the installation options and prefer installing from the plugin
version already resolved in the application's own node_modules, using
the files[].target mapping in the published public/r/nocobase-ai.json.
That keeps the installed source byte-identical to the canonical source
for the plugin the application runs, without network access or a source
checkout. Keep workspace materialization and hosted shadcn add for the
cases where they apply, and state the version risk of the latter.

Both documented installers refuse an existing target, so application
owned source is never overwritten; upgrades stay a three-way merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(ai-employee): assert the widened Registry plugin constraint

The Registry contract test still pinned `requiresPlugins` to the old
`>=0.0.1 <0.1.0` range, so widening it to `>=0.1.0-beta.0 <1.0.0` left
the suite red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): fix the tool context when the agent is created

Backend tools received `AgentRequest.context.agentContext`, so every
caller assembled that context itself and a request could substitute
another actor, session or set of services for the one the agent was
created with. `AgentContextProvider` now exposes `toolRuntimeContext()`,
`AgentServiceFactory` builds the context once from `actor`, `translate`,
`getHeader`, `execution` and `state`, and `AgentService` applies it
after the request spread, so a request cannot replace it.

`AIConversationService` and `SubAgentsDispatcher` pass `execution`/
`state` instead of building four separate contexts, and
`AIEmployeeAgentContextProvider` resolves the model through the
employee's own policy rather than requiring one on the request.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): lint the nocobase-ai Registry source as an application does

The Registry item is installed into an application's client/extensions and
linted there by the Portal configuration, but this package excluded
`registry/**` from its own lint, so installing the AI frontend extension
took a generated application from zero lint problems to 118. The exclusion
is gone and the source is linted here with the same rules, typed through
`tsconfig.registry.json`.

The fixes are behavioural, not cosmetic: refs are no longer written or read
during render, effects no longer call setState synchronously, untyped
server fields no longer stringify to `[object Object]`, promise-returning
handlers no longer float a rejection, and list keys no longer come from an
array index. A module that exports a component now exports only components,
with its contexts, hooks and helpers in a sibling module; the names exported
from the package entry points are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): classify agent configuration failures and keep the cause message

An agent with no usable model failed as `PROVIDER_ERROR: Agent execution
failed`, with the real message — `LLM service not found`, `LLM service not
configured` — buried in `cause`. Every consumer had to walk the cause chain and
match strings to separate a misconfigured application from a provider outage,
and `AIConversationService` reported the whole class as HTTP 500.

The message was overwritten by an argument named as though it were a fallback:
`normalizeAgentError` preferred `fallbackMessage` over `cause.message`, and
`executeInvoke` passed the constant `'Agent execution failed'`. `executeStream`
passed `provider.parseResponseError(error)`, which no provider overrides and
which returns `err.message`, so streaming reported the real failure all along.
The lifecycle test encoded that disagreement: `'prepare failed'` for `stream()`,
`'Agent execution failed'` for `invoke()`.

Classify by execution phase instead of by message. Anything thrown while
resolving the model, LLM service, or provider becomes `CONFIGURATION_ERROR`, so
a configuration failure added later needs no new string to match.
`normalizeAgentError` now prefers the cause's own message, and `executeInvoke`
parses the error the way `executeStream` does. `AgentServiceError.rootMessage`
returns the deepest message in the cause chain, and `AIConversationService` maps
the code to a status rather than reporting 500 for everything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(ai-employee): give invoke() a result contract and a structured answer option

`invoke()`, `resumeInvoke()` and `forkInvoke()` returned `Promise<unknown>`, so
a caller that needed the answer reached into the underlying graph state. That
state also carries the keys this package's own middleware contributes, so it
was never a contract a middleware could be added to. `SubAgentsDispatcher` did
exactly this, through `extractLastMessageText(result: any)`.

They now return `AgentInvokeResult`: the assistant turn as `message`, in the
same `AIMessageInput` shape the rest of the API uses, converted by the same
converter that persists it. The graph state stays internal.

`AgentInvokeRequest` takes a `responseFormat` Zod schema and the result carries
`structuredResponse`, typed from it. `LLMProvider.prepareChain` has applied
`withStructuredOutput` all along; nothing above it could ask for one. Only
`invoke()` accepts it — `stream()` has nowhere to put a structured value.

Guard `provider.parseResponseError()` in both error paths. A provider that does
not implement it made the error handler throw a TypeError over the failure it
was reporting, which is how the conversion failure behind this change surfaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat(ai-employee)!: declare tool dependencies as container tokens

A backend tool received the whole AgentContext and reached into it. Nothing was
declared, and two problems followed. A tool outside this package could not ask
for an application service at all — the default route builds the tool context
itself and `services` was a fixed set, so closing over a service at bootstrap
was the only way. And `ctx.database` handed every tool a full DatabaseManager,
scoped by nothing; no tool used it, it was simply reachable.

A tool now declares container tokens and gets them resolved:

    dependencies: { billing: billingServiceToken },
    invoke: async (ctx, args) => ctx.deps.billing.find(args.id, ctx.actor.id),

Nothing new registers them; these are the application's own tokens. TDeps is
inferred from `dependencies`, so `ctx.deps` is typed from the tokens — the
symbol property on ServiceToken is what makes that inference exact.

AgentService resolves each tool's declaration into that tool's own context, so
a tool reaches neither what another declared nor anything undeclared, and an
unresolvable token fails naming the tool and the token. A tool is bound to its
context when built rather than reading it from the invocation config, so an
`agentContext` key on a request now reaches nothing.

AgentContext narrows to this execution: deps, actor, state, logger, translate,
getHeader, availableSkills. The seven AppAgentServices wrappers go with it; the
built-in tools declare managerFactoryToken, repositoryFactoryToken or
aiManagerToken and call those managers directly. The data tools declare
dataServicesFactoryToken and build their reader with ctx.actor, so no tool
declares the database.

BREAKING CHANGE: AgentContext loses `ai`, `database`, `repositories` and
`services`, and its generics collapse to <TDeps>. buildTool takes the bound
context as its second argument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee): drop the hard-coded chat model lists

`LLMProviderMeta.models` had one consumer: `ai:listModels?model=EMBEDDING`,
which fills the knowledge base embedding model picker. Chat models are listed
live from each provider's own API through `ai:listProviderModels`, so the
`SupportedModel.LLM` entries were read by nothing while still needing to be
kept current — a stale list that reads as authoritative is worse than none.

Remove those entries, narrow `models` to `Partial<Record<SupportedModel.EMBEDDING,
string[]>>` so an LLM list cannot be reintroduced, and let `listModels` answer
only `model=EMBEDDING`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(ai-employee): delete the commented-out Tongyi provider

The file has been entirely commented out since it was ported, so
`export * from './llm-providers/tongyi.js'` exported nothing. Git history
holds the implementation if it is ever revived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): say what enabledModels constrains

"The configured name set is authoritative" sat in a paragraph that opened on
MCP server reconciliation and ended on `enabledModels`, so it read as a claim
that the model list is an enforced whitelist. Nothing enforces it: the list
scopes the selector, `ai:listAllEnabledModels` and the `resolveModel()`
fallback, while a caller naming a model explicitly is never checked against it.

Split the paragraph into the section it describes, state the list's reach in
the README and both Skill references, and drop the docs site's claim about a
built-in model catalog, which no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(ai-employee): remove the unreachable requireModel check

`ModelService.requireModel()` rejected a model that the service's
`enabledModels` did not list, but nothing called it — no route, no service,
only its own test. The invoke path resolves a caller-supplied model through
`resolveModel()`, which does not consult the list, so the check enforced
nothing while suggesting the list was enforced somewhere.

Its test file covered only this method; the legacy `recommended` mode it also
exercised is covered by the normalization and migration tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(ai-employee): remove the remaining uncalled ModelService methods

`getSupportedProvider()` and `randomUuid()` have no callers: the AI router
reaches only `listEnabled`, `listLLMProviders`, `listLLMServices`, `listModels`
and `listProviderModels`. The provider manager keeps its own
`getSupportedProvider`, which `listLLMServices` uses directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: separate a conversation turn, its transport, and one agent call

One sendMessages request was unpacked four times: the route parsed the body
into a ConversationExecution, AIConversationService parsed the same body
again, assembled an execution plus a hand-written state plus flat
webSearch/frontendTools/sessionId options, and resolveState() merged all
three through a fallback chain. sessionId, messages, messageId, model,
webSearch, frontendTools and important each had four homes, and the model was
resolved twice.

ConversationExecution becomes two types that do not overlap. ConversationTurn
is what a turn asks for, parsed only by parseTurn() in the route;
ConversationTransport carries the SSE target, abort signal, translate and
getHeader, and stops at the service, so an agent can no longer be handed a
stream target. A turn names no session: the conversation an agent runs in is
decided by whoever creates it, so sessionId lives once on
CreateEmployeeOptions and toAgentState() takes it as already decided.

The model is resolved once, by AgentServiceFactory, against the employee's
policy. AgentRequest therefore loses model and context: resolveLLM() takes no
argument and reads the agent's own state through the new
AgentContextProvider.state(), and the important retry prompt reads it too.
What is left on a request is what varies per call, plus runtime, the renamed
middleware channel carrying appendMessages.

Each service action now takes the few fields it reads rather than the whole
request body, and conversationAgentOptions() reads the conversation record's
systemPrompt, skillSettings and tools in one place instead of four copies of
the same type guards.

Two defects fell out. `from` was computed as
execution.sessionId === sessionId, but the route derived both from
input.sessionId, so the branch never chose sub-agent. Two empty
`if (shouldStream) {}` blocks are gone, along with the uncalled
toAgentRequest() helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: drop the unreachable per-conversation tool injection list

CreateEmployeeOptions.tools, and the tools it set on
AIEmployeeAgentContextProvider, merged names into the candidate tool set
beside the employee's own skillSettings.tools and enabledTools. Nothing ever
filled it: it came from conversation.options.tools, and no code writes that
key. AIConversationsManager.update() accepts only systemMessage,
skillSettings, conversationSettings and modelSettings, both callers of
create() pass the same subset, AIConversationsOptions does not declare it,
and the schema is created fresh by migration rather than carried over from
v2, so no stored row holds one either.

It could not have worked even when filled, because the per-conversation
skillSettings.tools filter runs afterwards: a name injected but absent from
that filter was discarded again by isToolSelected(), so for every name but a
system tool the two lists cancelled each other.

getAIEmployeeTools(), getEligibleToolNames() and conversationAgentOptions()
each lose a merge that could only ever add nothing, and what an employee may
use is decided in one place. CreateAgentOptions.tools, which names the tools
a fixed agent loads, is a different thing and is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: drop the AppAgentContext alias

`export type AppAgentContext = AgentContext` carried no type information.
AgentContext<TDeps = Record<string, never>> already defaults its one
parameter, so the alias was exactly the type it aliased, and a reader meeting
both names in one file had to check whether they differed. It read like a seam
for application-specific fields, but that seam was never real: a context is
built by createAgentContext() and handed to a tool as AgentContext, so a field
declared only on the alias would not exist at runtime. The form that carries
meaning here is the parameterized one, as AgentContext<SubAgentDeps> in
server/ai/sub-agents/shared.ts shows; the zero-dependency case needs no name.

Its comment moves to createAgentContext(), which is what actually establishes
the rule it described.

The boundary test now asserts the boundary rather than the name:
server/agent/context.ts must not import from hono, and must take its context
type from the library. It previously asserted only that the file contained the
string AppAgentContext, which a rename would have broken and which proved
nothing about what a tool receives.

Two latent type errors in the tests are fixed along the way, both invisible to
`pnpm typecheck` because tsconfig.json does not include tests/. defineTools'
type parameter is a tool's dependency token map, not its context, so
`defineTools<AgentContext<{}, {}>>` was wrong in both arity and kind; those
tools declare no dependencies, so the argument is dropped. withTestToolDeps()
returns AgentContext<TDeps> rather than an intersection that widened deps to
Record<string, never> & TDeps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee): name the conversation options each agent call passes

AIConversationService spread conversationAgentOptions() straight into
createAIEmployee(). Destructuring it instead names what a call site hands the
factory, so reading one of those calls no longer means opening the helper to
learn which options it supplies. getHeader is destructured from the transport
with translate for the same reason.

Behaviour is unchanged. The helper is pure and reads only conversation.options,
so calling it beside getConversation() rather than at agent creation moves
nothing that another step depends on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee): destructure getHeader in updateUserDecision too

The other four conversation actions read translate and getHeader off the
transport in one place; this one still reached for transport.getHeader at the
call site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: name the sub-agent handoff messages and set them only on a handoff

AgentState.messages and ConversationTurn.messages read like a third history
channel beside the checkpointer and AgentRequest.userMessages, and every turn
filled them. They are neither. Their one reader in the whole workspace is
dispatch-sub-agent-task, and they serve one flow: when a user ignores a
sub-agent's pending tool call and sends a new message, sendMessages resumes the
main agent with the tool decisions alone, so that message never enters the main
graph. It reaches the dispatch tool through the agent state instead, which
hands it to the sub-agent as appendMessages. The message answers the
sub-agent's pending question, so it belongs to that conversation and is
persisted there.

Both fields are handoffMessages now, and AIConversationService fills them only
on that path. isInterrupted() and reject() move ahead of createAIEmployee() --
both are repository work needing no agent -- so the service knows whether a
handoff is happening before it decides what the agent's state carries. Every
other turn leaves the field empty, a resend included: its messages are that
call's input and nothing more.

An ordering problem disappears with it. The agent used to be created before
cancelToolCall() prepended the cancelled-tool continuation, so state.messages
silently omitted messages that userMessages carried. That branch no longer puts
messages in the state at all, so there is nothing left to disagree.

sendMessages takes the incoming messages as its own parameter rather than on
the turn, beside sessionId, aiEmployee and stream. SubAgentTask.messages is
handoffMessages too, and the dispatch tool stops handing the same array over
twice: it rode both the task and the sub-agent's turn, while SubAgentsDispatcher
resolved them with a fallback that could never fire.

The handoff path had no test. One covers it now, and was checked against a
mutation of the branch it guards.

A tool reading ctx.state.messages, or a caller passing messages on a
ConversationTurn, uses handoffMessages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: type the resolved model on AgentState and check it at the boundary

AgentState.model was Record<string, unknown>. It has been since the v3
migration commit that introduced the type, and nothing chose it: ModelRef lives
in the plugin, so the library had no name for the shape it was carrying. The
looseness bought nothing -- its only writer puts a resolved model there, and
both readers narrowed it straight back out through isModelRef, a guard the
plugin had to keep and export.

It is { llmService: string; model: string } now, which is what it always held,
and the guard goes with its two call sites.

That guard was covering for something else. parseTurn checks the type of every
field it takes off the request except this one, which passed input.model
through untouched, so AgentState.model promised every tool a resolved reference
while carrying whatever the client had sent. The check happens there now, with
the rest: a model missing either half is dropped rather than carried, and only
the two fields the state holds survive. The test that guarded the old narrowing
moves to the route, where the check now is, and was verified against a mutation
of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: carry one AgentState from the route

ConversationTurn and AgentState had come to hold the same nine fields, and
toAgentState() copied one into the other by hand. Two names for one thing is a
concept to carry and a mapping to keep in step: a field added to AgentState
reached neither the turn nor the copy, silently.

AgentState is the only one now. parseAgentState() builds it where the request
body is parsed, it travels unchanged through AIConversationService, and each
field that is not the request's to decide is replaced exactly once, where the
decision belongs: AgentServiceFactory replaces model, having resolved it
against the employee's policy, and SubAgentsDispatcher replaces sessionId,
because a sub-agent runs in its own conversation.

ConversationTurn and toAgentState() are gone. CreateEmployeeOptions takes state
instead of turn and sessionId, and dispatch-sub-agent-task hands its own
ctx.state over whole rather than rebuilding it field by field.

AgentState.sessionId is required. A conversation is known by the time an agent
exists -- the route validates it, a fixed agent generates one -- so a tool no
longer reads it as possibly absent, and AIConversationService takes it from the
state rather than as a parameter beside it.

ConversationTransport stays separate and still stops at the conversation
service: what an execution is and how its output reaches the caller are not the
same thing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: group what the host lends an execution into AgentRuntime

AgentContext held logger, translate and getHeader beside actor, state and deps,
as if the four were the same kind of thing. They are not: actor and state say
what the execution is, deps is what the tool asked for, and those three are
only what the host lends it for as long as it runs.

They are AgentRuntime now, reached as ctx.runtime. CreateAgentContextOptions
takes the same object rather than the three fields loose, so a caller assembles
the runtime once instead of threading logger through every construction site
beside the state.

A tool reading ctx.logger, ctx.translate or ctx.getHeader reads
ctx.runtime.logger and so on. One building an AgentContext by hand supplies
runtime in place of the three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: call the context an agent binds its tools with agentContext

The same AgentContext went by three names on the way to a tool:
toolRuntimeContext on the provider contract and its two options, runtimeContext
on the fields holding it, and baseToolContext where AgentService read it back.
It is agentContext everywhere now.

AgentContextProvider.toolRuntimeContext() becomes the agentContext property. It
was already a value fixed when the AgentService is created and never
recomputed, which is what the rename made plain: a class cannot hold a field
and a method of one name, and the choice between them settled it. It is also
typed AgentContext rather than unknown, so AgentService no longer narrows what
it hands to buildTool.

FixedAgentContextOptions.agentContext is required, as the employee provider's
already was. Both providers were handed one at every construction site; the
optional half only existed to let state() fall back to an empty object, which a
required session had already made impossible.

The one runtimeContext that is not an AgentContext -- the rest of a request's
runtime bag in prepare() -- is requestRuntime, so the name no longer says
context at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: read the agent's state off its context, not a second accessor

AgentContextProvider.state() returned agentContext.state and existed only
because agentContext used to be unknown: a caller that wanted the state could
not reach it through the contract. Now that the property is typed, the accessor
is a second way to one value, so it is gone and its three callers read
agentContext.state directly.

tests/agent-tool-runtime-context.test.ts is agent-tool-context.test.ts, named
for what it covers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: take the execution's runtime whole when creating an agent

CreateEmployeeOptions listed translate and getHeader loose, and
AgentServiceFactory assembled an AgentRuntime out of them and its own logger at
each construction site. A caller could name only two of the three, so which
logger an agent's tools would write to was the factory's to decide rather than
the caller's to say.

Both options take runtime: AgentRuntime now, required for createAIEmployee and
optional for createAgent, which falls back to the application's logger because
a fixed agent serves no request. SubAgentExecutionOptions takes the same, and
dispatch-sub-agent-task hands its own ctx.runtime over whole, as it already
does with ctx.state: a sub-agent inherits both what the execution is and what
the host lent it.

AIConversationService builds one runtime per action, in agentRuntime(), and
passes it to the agent and to its own tool context alike.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: stop restating on the provider's options what its context holds

Six of AIEmployeeAgentContextProviderOptions' fields were the values
agentContext was built from, handed over a second time: sessionId is
state.sessionId, webSearch is state.webSearch, frontendTools is
state.frontendTools, actor is actor, and translate and getHeader are the
runtime's. The provider copied each into a field of its own at construction, so
two records of one value could come apart with nothing to catch it.

They are gone. The provider derives all six from agentContext through private
getters, AgentServiceFactory stops passing them, and what is left on the
options is what the context genuinely does not carry: the employee, its
managers and repositories, the conversation identity, and the
conversation-level systemMessage and skillSettings.

One inconsistency fell out with them. The system prompt read the timezone
straight from the x-timezone header, while parseAgentState had already resolved
it as the request body's value falling back to that header, and every tool
reads the resolved one. A caller that sent a timezone in the body got it
honoured by query-data and ignored by the prompt. The prompt reads
state.timezone now, so both agree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: depend on the manager that owns model policy, not a closure

AIEmployeeAgentContextProviderOptions.resolveModel was a closure, and
AgentServiceFactory built it by binding the employee to
aiEmployeesManager.resolveModel. The provider already holds that employee and
already depends on four other managers directly, so the closure was a wrapper
around a manager the provider could simply be given: it hid which component
owns the policy, and it left the provider unable to reach anything else on that
manager without another callback beside it.

The option is aiEmployeesManager: AIEmployeesManager, and resolveLLM() calls
resolveModel(this.employee, ...) itself. The employee's configuration still
decides the model, and a request still may ask for one without widening what
the employee allows; only the route to the code that enforces it is direct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): describe the agent context work as one change, not as its steps

Thirteen changesets narrated this branch's agent-context work commit by commit,
and the later ones renamed what the earlier ones had just introduced. A reader
of the published CHANGELOG would have met ConversationTurn, toolRuntimeContext()
and AgentContextProvider.state() and then been told they were renamed or
removed, though none of the three exists at the merge base and none would ever
ship. A changeset summary is copied verbatim into the CHANGELOG for a reader
outside the team, so it should describe where the release lands, not the route
taken to it.

They are two, written against the released contract and in the vocabulary that
ships: one for the execution state a request becomes, one for the context an
agent binds its tools with. The wording superseded by later commits is gone with
them -- the option shapes, the intermediate type names, and the claim that a
request's model is what the employee policy is applied to.

ai-employee-tool-dependencies.md keeps its own entry, its AgentContext field
list corrected to deps, actor, state, runtime and availableSkills.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: require a fixed agent's session and drop its username

CreateAgentOptions.sessionId was optional and generated with randomUUID() when
absent, but every caller of createAgent() names the conversation the agent runs
in -- an agent whose session nobody knows has nowhere to persist to and nothing
to resume. It is required, and the whole options object with it.

username is gone. It reached CurrentConversation.username, which names the
assistant role a conversation's messages carry: an employee agent has one, its
username, and a fixed agent has none. Nothing ever set it, and
FixedAgentContextOptions loses it too, since createAgent is what constructs
that provider.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: require a fixed agent's actor and runtime

createAgent() filled in what a caller left out: a missing runtime became the
application's own logger, and a missing actor became { id: 0, roles: [],
isRoot: true }. The second is the one that matters -- an agent created without
an actor ran as root, and nothing at the call site said so.

Both are required. Every caller knows who the agent runs as and what the host
is lending it, so neither default was serving anyone, and an implicit root is
not a default worth having.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: require an employee agent's actor too

createAIEmployee() filled a missing actor in with { id: 0, roles: [], isRoot:
true }, the same implicit root createAgent() had. All four callers -- three in
AIConversationService and one in SubAgentsDispatcher -- pass an actor, so the
default served nobody while leaving every reader of the factory to wonder who
an agent runs as when the option is absent.

CreateEmployeeOptions.actor is required, and no implicit root remains anywhere
in the plugin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): cut the comments back to what the code cannot say

Reviewing the comments this branch added found three kinds of problem. Some
were outdated: ConversationTurn and the word "turn" survived in comments after
the type was deleted, and ai-conversation-service.ts still opened by describing
exported functions that mirror an older action layer, which they are not. Some
said the same thing three times over -- the model story was told by
AgentState.model, by parseAgentState and by the factory that resolves it, and
the sub-agent handover was told from both sides of the same call. And some ran
four lines where the type or the function name already carried the noun.

Each point is made once now, where it belongs: on the type that promises it, or
at the line that would otherwise surprise a reader. Comments that record a
decision are kept and shortened -- why the model is resolved in exactly one
place, why the interruption is resolved before the agent exists, why
options.tools is deliberately absent. A hundred lines of comment become
thirty-three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): correct the Skill where it describes the changed agent contracts

The Skill ships with the package and synchronizes into an installed
application's .agents/skills/, so an agent building against it writes what it
says. It said several things this branch makes untrue, and a tool written from
it would not have compiled: ctx.logger and ctx.translate are under ctx.runtime,
ctx.state.messages is handoffMessages, ctx.state.sessionId is always present,
and ctx.state.model is a resolved { llmService, model }.

references/agent-service.md had the same problem at the factory and provider
boundaries. CreateAIEmployeeOptions listed seven fields that no longer exist and
omitted the two now required; CreateAgentOptions still had username and optional
sessionId, actor and runtime; AgentRequest still carried model and context; and
AgentContextProvider was documented with toolRuntimeContext() and a
resolveLLM(request) that takes an argument.

Each contract is corrected against the shipped types, and the rules around them
now say where the decision is made: the model is resolved once when the agent is
created, the actor is required because there is no implicit root, and a request
cannot swap what the agent was created with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): rewrite the Skill as a build order

The Skill was organized by which part of the package a fact belonged to, so
an agent asked to build something could read all of it without learning what
to do first. `SKILL.md` is 92 lines now and says, in order: who owns what,
what must exist first, which resource kind answers which request, the path
through them, the safety rules, and how to tell it worked.

The references split by what the reader is doing. `capabilities.md` covers
everything an App declares plus the whole `config.yml` `ai` block;
`chat-surfaces.md` absorbs `frontend-registry.md`; `server-runs.md` absorbs
`agent-service.md`. `contracts.md` dissolves into the three so each contract
sits beside the guidance that uses it, `index.md` goes because `SKILL.md`
routes, and `api-reference.md` is demoted to the case it actually serves.

Three claims were untrue and are gone: Skill directories do not discover
tools, `ai:testFlight` is not implemented, and the built-in list named ten of
nineteen tools and none of the three built-in Skills.

Four things an agent had to guess are now written down: how to obtain a real
model list from each provider at configuration time, where a key goes and how
to verify it is ignored, that avatar keys come from the plugin's own 60 and an
unknown one silently falls back, and that chat attachments default onto the
application's general upload disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): state what the data tools need before they can see a table

The built-in data tools read only collections registered for authorization
under the two-part name `<connection>.<collection>`, with the `read` action
kept and an actual grant for the current user. The catalog drops any
registration whose name does not split into exactly two dot-separated parts,
so `authz.db.collections.add({ name: 'leads' })` — how the rest of an
application registers a collection — leaves the table invisible here.

Nothing raises when it is missed. Discovery omits the collection, so the
assistant reports that a table the application's own pages read does not
exist. Neither version of the Skill said any of this.

Also say when to prefer a form the user submits over a tool that writes: the
values came out of a screenshot or a pasted note, which is where extraction
is least reliable and where the fields are exactly what a person can check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(ai-employee): read the executed readiness example from its one home

The test read the same integration example out of two documents and asserted
they matched, which was how the duplication stayed honest. The Skill now
publishes it once, in `chat-surfaces.md` under the readiness gate, so the
equality assertion has nothing to compare and the second path no longer
exists. The example is still executed against the real provider, which is the
part that matters.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): say that nothing warns when a model cannot read the image

`AIModel` carries `supportWebSearch` and no equivalent for image input, so a
chat with attachments enabled and a text-only model selected looks correct
until the provider rejects the turn. The Skill now says so where it tells an
App to enable attachments, and says to make the employee's default model one
that accepts images when the flow starts from a picture.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): correct the line count in the Skill rewrite changeset

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(app-skills): name the AI Employee plugin in the capability map

`nocobase-app-development` is where an agent starts almost every application
feature, and its "Check the installed plugins first" table is how it learns a
capability already has a plugin behind it. The table named workflow,
notification, authorization, authentication, file, i18n, users and
`@nocobase/db`, and said nothing about AI.

So "let an assistant read this screenshot and file the lead" went straight to
hand-written code. `@nocobase/app-plugin-ai-employee` ships its own Skill and
that Skill synchronizes into every application depending on the plugin, but
the one lookup table that would have pointed at it was silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): drop the scenario the Skill was reviewed against

The rewrite was checked by walking one lead-intake build end to end, and that
scenario ended up written into the examples: an employee that files leads
from screenshots, a `create-lead` tool with a `wechat | whatsapp | email |
note` source enum, `main.leads`, a `company-search` MCP server, `Sales AI`
settings, a `crm` application name. A Skill ships to every application, and
naming one vertical in every example steers an agent toward it and reads as
a claim about what the plugin is for.

The examples are a neutral order desk now: `create-order`, `order-intake`,
`main.orders`, `@acme/example-app`. The capability sentences keep what is
actually true of the plugin — it accepts a dropped file, images and PDFs
reach the model directly — without describing a product built on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): stop reporting a web search that never ran

`subAgentWebSearch` asked for `webSearch: true` and invoked the model. That
flag reaches `modelOptions.builtIn.webSearch`, which only six of the thirteen
registered providers read. The other seven ignore it silently, and the
request went through anyway: a model given a retrieval system prompt answers
from training data, complete with a Sources list, and the tool returned
`status: 'success'`. `openai-completions` — what a gateway is normally
configured with — is one of the seven.

The tool now checks `LLMProviderMeta.supportWebSearch`, and `webSearchModels`
where a provider declares one, before invoking. Unsupported returns an error
naming what did not happen and what to use instead.

Two more in the same area:

`ai.mcpServers` is documented to take `${NAME}` and is the only place MCP can
be configured, but nothing expanded it — a bearer token shipped with those
characters in it. It now uses the same expansion as `ai.llmServices`.

`enabledModels` in `config.yml` took effect only when a service row was first
created, silently thereafter. A service first created without it ends up with
zero usable models and no way to fix that from config. The new per-service
`overrideEnabledModels` reapplies the configured list on every load; it
defaults to false, and governs the model list alone, not whether a service is
enabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): send images to DeepSeek

`DeepSeekProvider.isApiSupportedAttachment()` returned false for everything,
so an image never reached the model. It did not reach the document loader
either, because `.png` is not a document extension, so a dropped screenshot
produced a system message saying the assistant cannot parse `image/png` —
from a provider that can.

Images now go as content blocks, matching every other image-capable provider.
Documents keep going through the loader, where this provider's text
extraction already works.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(templates): remove the ai/mcp placeholder

The directory implied MCP servers could be declared as files in it. They
cannot: the AI Employee plugin never passes a `mcpDirectory` to its resource
registrar, so that loader never runs, and `ai.mcpServers` in `config.yml` is
the only path. An empty directory implying a second one is worse than none.

No changeset: neither template lists `ai/` in `files`, and `create-app`
scaffolds nothing there, so this never reached a generated application. It is
source in this repository that a reader could believe.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): correct the Skill against the fourth review

An independent reviewer built an application from the Skill and checked each
claim against the code. Two findings were things an agent would have shipped
without noticing, and both came from the same blind spot: the Skill described
the function it pointed at rather than what the layer below does with it.

Web search was backwards. `webSearch` is not a second mechanism — it adds the
same `subAgentWebSearch` tool, and both need the provider's built-in search,
which six of thirteen providers have. Now one tool with two switches, with the
capability table beside it.

`enabledModels` was described as if `config.yml` won. It applies when the
service row is created and is ignored on every reload after, so a corrected
model id never lands and a service created without a list stays at zero
models. The consequence is stated, the "just omit it" fallback is gone, and
`overrideEnabledModels` is documented.

Also corrected, each checked against source: a tool named by any Skill is not
a base tool until that Skill loads; `autoCall` is read only for `CUSTOM` and
overrides `ASK` there; `AIPageContextScope` must be an ancestor of the chat;
`useAIForm` returns only a ref; a chat without `defaultEmployee` opens on
`atlas`; registration lives in `server/plugins.ts` and `client/plugins.ts`;
knowledge base needs a plugin that enables the feature; an unregistered
provider key is dropped silently; PDF and web-search support vary by provider;
the data tools have hard capacity limits.

Three limitations an application cannot fix are now stated rather than left
for deployment: a built server does not read `.env`, `ai/skills` is not copied
into `dist/`, and every authenticated user can talk to every enabled employee.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): make the built-in catalog an inventory

The Skill listed nineteen tools with one line each and gave the three
built-in Skills a single sentence between them, which left the thing an App
actually has to decide — whether a capability belongs in an employee's
`tools` or behind a Skill — undocumented.

There is now a section on how the two relate: a Tool is code and the only
thing that acts, a Skill is a procedure plus a list of names and implements
nothing, and four properties decide reachability. The one that surprises
people is stated plainly: a tool named by any registered Skill leaves every
employee's base set until that Skill is loaded, so putting a tool in a Skill
takes it away from employees that only list it.

Both catalogs are complete. Each Skill shows the tools it names, what its
procedure is for, and what its body carries beyond the list. The tools are in
six families with scope, permission and execution side, including the two
that read oddly and are correct: `suggestions` is `ASK` because the ask is
the interaction, and `getSkill` is exempt from the gating it performs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): say whose ai/skills the build leaves behind

The limitation note read as though Skill directories were dropped in general.
The plugin's own build copies `ai/skills` into its `dist/`, so built-in Skills
ship normally; it is the application build that does not carry the
application's own `ai/`. Naming which one avoids reading this as a defect in
the plugin's Skills.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): document that config only seeds a service's model list

`overrideEnabledModels` shipped without the places that describe the reload
behaviour catching up, so the official page, the plugin README and the
library's contract reference all still said an administrator's model list is
preserved and stopped there. That sentence is true and is not the part that
matters: `enabledModels` and `enabled` reach a service only when its record is
first created, so a model id written wrongly the first time cannot be fixed
from `config.yml` at all, and nothing reports it.

All three now say that, and document the switch that opts one service out —
per service, default false, model list only, `enabled` left with the
administrator. The Chinese page also gains the consequence of omitting
`enabledModels`, which is a service with no usable model rather than a
sensible default, and a note that this switch plus `sort` is what decides the
chat's default model.

The English page is still a placeholder, so there is no translation to keep in
step; `en` has 61 such pages and the previous edit to this page was likewise
Chinese-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): call the three gaps open defects, not the way things are

The section read as though a built server never reading `.env`, an
application's `ai/skills` never reaching `dist/`, and every user reaching every
employee were design. They are defects with fixes planned. An App still cannot
close them today, so the workarounds stand, but an App should not be built
around them as permanent — and a reader on a later version should check
whether a gap still applies rather than trusting a Skill that shipped earlier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(templates): remove the unreferenced tsconfig.ai.json

`rootDir: "ai"`, `outDir: "dist/ai"` — it was written to compile the
application's `ai/` into the build, and nothing ever invoked it. No script,
no workflow, no other tsconfig references it, and neither template lists it
in `files`, so it never reached a generated application either.

Leaving it implies the application build carries `ai/` when it does not, and
it would not have been the fix anyway: a Skill is `SKILL.md`, and tsc emits
only TypeScript. Getting App Skills into `dist/` needs a copy step, the way
the plugin's own build copies its `ai/skills`.

No changeset, for the same reason as the `ai/mcp` placeholder: this reached no
published artifact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(app-tools): carry the application's AI Skills into the build

`tsc` emits only TypeScript and a Skill is Markdown, so nothing carried
`ai/skills` into `dist`. The AI Employee plugin reads the application's Skills
from `<applicationRoot>/ai/skills`, and a deployed server's application root is
`dist`, since it resolves its own paths from `dist/server`. The directory was
never there, and a missing Skill directory is logged at debug level and
skipped — so an application's Skills worked in development and disappeared on
deployment with nothing reported.

The build now copies them, beside the step that copies external Collection
metadata for the same reason.

Markdown travels at any depth, not only `SKILL.md`. A Skill may split its
detail into a `references/` directory that `SKILL.md` links to — this plugin's
own Skill is shaped that way — and such a Skill deployed without those pages is
worse than one that is absent: absent, it never loads; present and broken, the
model follows a link into nothing.

Code does not travel. Tools are registered in code and a Skill only names them,
so a `.ts` or a fixture under a Skill directory is nothing a deployment reads.
An application with no `ai/`, which is every application `create-app`
generates, copies nothing and the step stays silent.

`build-hooks.test.ts` pins the pipeline's step sequence, so it gains the new
step — that assertion is why this could not be added quietly. Verified against
a real build: a probe Skill with `references/detail.md` and a stray `.ts`
produced both Markdown files in `dist/ai/skills` and not the `.ts`.

The Skill drops this from its known gaps and says what the build now does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): hand permission registration back to the Skills that own it

The section on reaching business data taught `authz.db.collections.add`,
grants and narrowed action lists — another plugin's API, in a Skill whose own
`description` hands permissions back to `nocobase-app-development`. Two Skills
explaining one API in different words is how their disagreement about the
resource name stayed invisible.

Registering and granting now point at `nocobase-app-plugin-authorization`, and
at `nocobase-app-development` for where that work sits. What stays is what no
other Skill can know: these tools read only resource ids shaped
`<connection>.<collection>`, that id is the application-wide one rather than a
private key — so renaming for these tools means renaming everywhere, and
registering both names produces two resources that grant separately — and
anything unreachable is invisible rather than refused, which is why a table
the application's own pages read fine can be absent with nothing logged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(ai-employee): pin what a bare authorization resource name does

The data tools keep only resource ids that split into exactly two parts, and
nothing tested the other case — which is how a constraint this consequential
stayed invisible while the authorization plugin's own Skill taught the bare
form in every example.

Registering and granting a bare `orders` both succeed; the disagreement is
entirely on this side. What it produces is worse than a missing table: the
connection goes with it, so `getDataSources` returns an empty list and the
assistant reports having no data sources at all. Anyone debugging that looks
at the database configuration rather than the grant. Only a direct query says
what actually happened.

The requirement itself is deliberate, not an oversight — these tools address a
collection as `{ dataSource, collection }`, and a flat catalog of bare names
cannot say which connection an `orders` belongs to. The Skill now says that,
and notes the asymmetry worth knowing: a query defaults `dataSource` to `main`
while a registration is never assumed to mean it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): name the symptom the tools actually produce, and drop a claim the build fixed

Two stale lines, both mine.

The prerequisite said a collection registered under a bare name is "simply
absent", which understates it. The probe test added alongside shows discovery
returns no data sources at all — the connection goes with the collection — so
the symptom an agent matches against is "this application has no databases",
not "this table is missing". Naming the wrong symptom sends the reader to the
wrong place.

The source map still told a reader to check whether the build carried
`ai/skills`, written when it did not. The build copies it now, so the sentence
described a defect that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): say what an agent run with nobody watching has to get right

A caller driving an agent from a job, a schedule or a workflow node needs four
things from this plugin, and none of them were written down. What that caller
is — how it schedules, where it puts the result — stays its own concern; this
is only what the plugin requires of it.

Use `invoke()`: `stream()` advances only while consumed, so an unattended
caller that forgets to drain it stalls holding an open conversation. Pass the
signal the surrounding system already cancels with, because there is no
wall-clock limit and the only automatic stop is a 200-step recursion limit.
Do not let an interrupt happen — activate only tools that run automatically —
and if one must be resumed, decide explicitly rather than approving whatever
is pending, which turns every `ASK` into an `ALLOW` where nobody can see it.
Anything the run should do while it runs is an ordinary registered tool;
`responseFormat` already covers wanting the answer at the end.

One correction fell out. `invoke()` was documented as returning `message:
null` for a turn suspended awaiting a tool decision. It does not — it rejects
with a `GraphInterrupt`, passed through rather than wrapped, so a caller
matching on `AgentServiceErrorCode` never sees it.

The error contract is documented for the first time, since retrying is the
caller's decision. `GRAPH_RECURSION_ERROR` and `EMPTY_RESPONSE` carry
`retryable: true` — a model is not deterministic, so another attempt may take
fewer steps or actually answer — and everything else defaults to false and
fails identically the second time. Two declared codes are mapped to HTTP
statuses but never raised from the agent path, which is worth knowing before
waiting for one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): record a human-in-the-loop interrupt from invoke()

A top-level graph reports an interrupt by returning it under `__interrupt__`
rather than throwing, and `executeInvoke` read only the answer out of that
state. The paused tool calls stayed in `init` and no `interruptId` reached
their assistant message, so a reviewer's decision matched nothing, a resume
could not rebuild its command, and the caller received the tool-calling turn
as a finished answer. `stream()` never had this problem; `sendMessages` and
`resendMessages` with `stream: false` did.

`invoke()` now records the interrupt through the same `recordInterrupt` the
stream path uses, and reports it as `AgentInvokeResult.interrupt`. An
interrupt names the conversation it paused but not the message, and a
sub-agent's saved messages are reported only through the writer, so invoke
observes the writer to map each conversation to its message. It forwards
every event to the caller's writer, or without one to the enclosing graph's,
because a writer set explicitly would otherwise replace the one LangGraph
inherits. A nested agent still rethrows its interrupt, now recognised with
`isGraphInterrupt`, for the enclosing execution to record.

The Skill said an interrupted `invoke()` rejects with `GraphInterrupt`. That
held only for a nested agent; it now describes the result field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): finish following invoke() now reporting its interrupts

The commit that made `invoke()` record and report a human-in-the-loop pause
updated the Skill's description of the result. Three places still followed
the old behaviour.

The release's own changeset for the Skill said an interrupted `invoke()`
rejects with a `GraphInterrupt`. That sits in the same CHANGELOG as the entry
saying it now resolves with `interrupt`, so the two would have contradicted
each other in print.

`server-runs.md` told a caller to decide per action without saying what an
action holds. It names the tool call — `id` and `name` — and not its
arguments, which come from `message.toolCalls` on the same result, joined on
`id`. It also did not say what an abandoned pause leaves: the calls stay
`interrupted`, and `invoke()` does not clear them before a new turn the way the
chat route does with `cancelToolCall()`, so a new turn goes into a new
conversation rather than into the paused one.

`api-reference.md` says a `stream: false` send closes with an empty body.
That still holds, but reading history afterwards now shows a difference it
could not before: a paused run's tool calls appear as `interrupted` rather
than as a finished turn, and are answered through the decision endpoints.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): stop saying roles narrow sub-agent dispatch

The Skill said roles limit which employees the sub-agent tools can delegate
to, unlike the chat list. They do not. Both copies of
`buildAccessibleEmployeeFilter` return the same filter from their root branch
and their non-root branch — `enabled`, `category: 'business'`,
`deprecated: false` — so the role check is a no-op, and delegation is no more
restricted than the chat.

The known-gap entry now says what is actually true: an employee record has a
`roles` field and nothing reads it. Listing, starting a conversation, and
delegating all ignore it, so each tool's own check against `ctx.actor` is the
only boundary.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): make the employee example follow the gating rule, and drop unfinished access control

The employee example listed `create-order` and `dataSourceQuery` in `tools`.
The first is named by the example's own `order-intake` Skill and the second by
the built-in `data-query` Skill, so under the rule the same reference explains
— a tool any Skill names stays behind that Skill until it is loaded — both
entries did nothing. The second also carried `autoCall`, which is ignored for
a `SPECIFIED` tool. The example now declares only its Skill and says why, and
the shortest path says to list a tool in `tools` only when no Skill names it.

Role-based visibility of employees is still being built, so the Skill no
longer describes it: the known-gap entry about which employees a user can
reach is gone, `roles` is no longer listed among the fields administered in
AI settings, and the sub-agent tools are described by what they filter on
rather than by comparison with roles. Telling a tool to authorize against
`ctx.actor` stays, in the safety rules and completion checks, where it holds
regardless.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): let the user choose where an LLM key lives

Rank three places for the key: a system environment variable behind
${NAME}, the value in an ignored and untracked config.yml, and .env,
which works only under pnpm dev in this version. Check that the files
are ignored first, and hand the user placeholder commands to run in
their own terminal so the key never passes through the agent. State
that changes take effect on restart, and that a deployment configures
its own config.yml and service environment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): say configuration changes take effect on restart in the README

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): say configuration changes take effect on restart, and rank where the key lives

The AI employee pages described editing config.yml followed by a
configuration reload. Say instead that the server reads its environment,
config.yml and .env when it starts, so any change needs a restart, and a
new environment variable needs a sourced or new terminal first.

The quick start's key step now ranks three places for the key: a system
environment variable, the value in an ignored and untracked config.yml,
and .env, which only pnpm dev supports in this version. It checks that
the files are ignored, gives placeholder commands the user runs in their
own terminal, and says a deployment configures its own config.yml and
service environment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): pause a fixed agent on a tool that asks

createAgent() passed its tools through without resolving `auto`, and
the interrupt check pauses only on `auto === false`, so an ASK tool ran
without review. Resolve it from defaultPermission, as the employee path
does for a tool with no preset.

Resolving it exposed the second half: a fixed agent had no checkpointer,
so the pause failed with "No checkpointer set". Give it the plugin's
checkpoint tables under the default persistence, and an in-process
MemorySaver beside a caller-supplied one.

The factory memory test fed AIMessage objects to FakeListChatModel,
which takes strings; the resulting content could not be restored from a
checkpoint, so it now passes strings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): export AgentServiceError from the server entry

A caller retrying an agent run decides from `code` and `retryable`, but
the class and its code union were reachable only by deep import. Export
both beside the other agent call types.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): keep a disabled service disabled under overrideEnabledModels

The override passed `service.enabled ?? current.enabled`, so a config
that sets `enabled: true` re-enabled a service the administrator had
turned off. Keep `current.enabled`, which is what the comment, the test
name and the docs already claimed, and cover the case the existing test
missed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): send a PDF to Ollama as extracted text

@langchain/ollama converts only `text` and `image_url` content and throws
"Unsupported content type" on a `file` block, which is how the base
provider sends a PDF. Accept only images as API attachments, so
documents go through the loader.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): resolve inherited subjects for the data tools

DataServices called authz.for() with the user and authenticated:* only.
The request middleware adds authz.subjects.resolveFor(principal), so a
grant to a team reached pages but not the assistant. Resolve the same
subjects, once per actor, and cover a team-only grant.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): correct the Skill on running an agent unattended

Say what a fixed agent does with ALLOW and ASK now that it pauses. Name
the GENERAL tools that always pause an employee run, suggestions and
formFiller, and show the skillSettings allowlist that excludes them; a
test proves the allowlist leaves them out and keeps the named tools.

Replace "an aborted attempt leaves nothing half-written" with what is
actually kept, import AgentServiceError from the entry, and correct the
EMPTY_RESPONSE and PROVIDER_ERROR guidance. Check allowedDecisions in
the interrupt example, explain cancelToolCall(), and add a decision-table
row and completion checks for unattended runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): keep an LLM key out of the transcript

"Never passes through the agent" could not hold: the agent's shell
inherits a variable set before the session, and option 2 puts the key
in a file the agent edits. Promise repository and transcript instead,
and name the commands that print a key.

Replace the sed -i commands, which failed on a missing or symlinked
profile and mangled keys containing & or |. The new ones were run
twice each against a missing, a symlinked and an existing profile
under zsh and bash. Explain why pnpm dev's own restart and a server the
agent starts miss a new variable, and correct what the build writes to
dist/.env.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): correct the Skill against the fifth review

State that getSkill returns the SKILL.md body only, that a relative
ai.skills.paths and tools/ names differ between development and a build,
that an existing service's options and modelOptions are replaced on
every load, and what the PDF column actually describes. Show how a
service authorizes from ctx.actor with resolveFor, drop the claim that
a failed query tells unregistered from ungranted, and add the two autoCall
details a CUSTOM tool trips over.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): require a session and AI settings access on /api/ai

Run every /api/ai action behind authentication.required() and read the
actor from the verified session. Gate the actions behind the AI settings
page on page:ai.settings access, leaving chat actions open to every
signed-in user.

Narrow llmServices to updateEnabled and updateEnabledModels on a
configured service, since services are defined in config.yml, and make
aiMcpServers:testConnection test a configured server by name or a
remote server inline, never an inline stdio command.

Align the Skill's API reference, the settings-page note and the README
with these rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): say the data services resolve inherited subjects

The internal note still said DataServices supplies only the user and
authenticated:* to authz.for(); since 7f4a6d6a it also resolves the
principal's inherited subjects, as the request middleware does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): align the Chinese docs with the Skill and the narrowed API

Replace the quick start's sed -i key commands, which failed on a missing
or symlinked profile, with the ones the Skill now gives, and add what
pnpm dev's own restart and the agent's shell miss. Correct what the
build writes to dist/.env.

Say that an existing LLM service's options and modelOptions are
replaced on each start, that the settings page and its API need
ai.settings access, and that a Skill is one file to the model.
Rewrite the server extension page against the current factory options
and add the unattended-run rules, and fix the employee example to
follow the Skill gating rule.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): have the agent build the key command for the user's environment

The Skill shipped fixed zsh, bash, PowerShell and perl commands, which
assumed a shell and an operating system the user might not have. Replace
them with what the agent has to work out first — operating system, shell,
the startup file that shell reads, where the server takes its environment
from, confirmed with the user — and the properties the command it builds
must have, verified with a fake value or replaced by an editor
instruction. The never-print list names kinds of command rather than one
shell's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): let a fixed agent load the Skills it is given

createAgent({ skills }) added the Skills' tools but no getSkill, and its
context had no availableSkills, so a Skill body never reached the model.
Inject getSkill bound to the selected Skills and list them in the system
prompt, through a formatSkillsPrompt() the employee prompt now shares.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): report a missing model at creation as a configuration error

Both factory methods resolve the model when the agent is created and
threw a plain Error there, so the error contract held only for failures
during an execution. Wrap them with toConfigurationError, which a chat
reports as 503.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(ai-employee): cover a fixed agent's pause under database persistence

The permission tests all passed a MemoryConversationPersistence, so only
the in-process checkpointer was exercised. Run the plugin migrations,
pause on an ASK tool with the default persistence, and resume from a
newly created AgentService.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(ai-employee): fail on an /api/ai action nobody classified

The settings-page gate lists the actions it protects and lets the rest
through, and its tests only walked that list, so a new admin action
left off it would pass. Classify every registered route as gated,
self-guarded or open to signed-in users, and fail on one in none, two,
or a list entry with no route.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): keep the MCP enable switch and tool permissions across restarts

MCP servers lived in the in-memory repository createAIManager builds,
registerMCP rewrote enabled: true on every sync, and tool permissions
were a process-local map, so both reset on each start.

Add MCPServerManager.switchRepository() and move onto aiMcpClients at
start, before the sync. Keep an existing server's enabled on sync, as
LLM services do, and save a tool permission on its server's row in a
new toolPermissions column, reloaded on rebuildClient.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): base AI file previews on the uploader and AI settings access

Serve a file to its uploader; require page:ai.settings access for
anyone else and for a file with no uploader, instead of the session's
isRoot. Put a cached canAccessAISettings on the request so the settings
gate and the preview ask the same question once.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): correct the Skill against the sixth review

Say an unattended run needs a real, authorized user; list the whole
data-query/data-metadata chain in the allowlist example and prove it
in the test; name every tool that pauses a server-side run, including
MCP and frontend tools; note an aborted tool's misleading status; loop
over repeated interrupts; cover creation-time configuration errors.

Describe MCP tool scope, naming, default permission and persistence;
switch the key command to a hidden prompt with parser round-trip rules;
list the /api/ai access groups completely; stop presenting the context
provider and persistence as App extension points.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): say the floating trigger ignores defaultEmployee

AIChatFloatingTrigger opens on its own aiEmployee, falling back to
employees[0], and never reads the provider's defaultEmployee, so a
floating chat has to pass the username to both. Also correct the claim
that MCP tools reach every employee: a saved tool selection is a fixed
list, and a tool discovered afterwards stays off for that employee.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): align the Chinese docs with the sixth review

Rewrite the tool page against defineTools with dependencies, ctx.deps
and the three things a writing tool owns. Describe the settings as
sidebar pages and the employee editor as it is; state that MCP state
persists and how MCP tools are named and permitted; add the readiness
gate and defaultEmployee to the chat examples; fix the baseURL example,
registration order, relative skill paths and several smaller claims.

Switch the quick start's key commands to a hidden prompt, tested under
zsh and bash with a value containing quotes, $, a backslash and a
backtick and read back with the app's own .env and YAML parsers; bring
the server extension page in line with the unattended-run rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): start without an MCP server that cannot be reached

initializeConnections() threw on the first server that failed to
connect, and it runs at start, so one unreachable MCP server stopped
the application. Pass onConnectionError to skip that server with a
warning naming it, keep the servers that connected, and log rather
than throw if initialization fails outright. testConnection keeps
reporting failures.

The provider restart test no longer stubs the connection, so it also
proves the start survives an unreachable server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): open a floating chat on the chat's defaultEmployee

AIChatFloatingTrigger chose aiEmployee ?? employees[0] itself and never
saw the provider's defaultEmployee. Let a trigger without aiEmployee
leave the choice to AIChatProvider.triggerTask, which resolves the
default, and make AIEmployeeTaskTrigger.aiEmployee optional.

Also fall back to defaultEmployee before employees[0] when the initial
selection is missing, since it is made on the first render and a chat
mounted before employees loaded landed on the first one. Rebuild the
Registry item, and drop the "pass it to both" advice from the Skill and
the Chinese docs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): show and send the models an employee is limited to

The server swaps a model outside an employee's modelSettings for the
first allowed one, but the chat listed and defaulted to the global
models, so it displayed one model and answered from another. Resolve
the chat's model list through getEmployeeModels(), in the employee's
order, for the selector, the current model, the send and task triggers.

A send compares the stored selection with the resolved model and
silently drops the message when they differ, so keep the stored
selection in step with the resolved model.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): keep MCP connection URLs out of the log

The connection warnings handed the adapter's error to the logger, and
its message quotes the full server URL. Log the server name and a
reason whose URLs are cut back to their origin instead, and test that a
fake token, password, user and path in the URL never reach the logger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): send from a chat mounted before configuration loaded

A chat mounted before employees and models loaded dropped every send:
its stored employee stayed the 'assistant' placeholder, which the send
guard rejected, and the cached Chat's transport read ai.employees and
ai.models from the render it was created in, both still empty. Keep the
stored employee in step with the resolved one, drop the placeholder,
and read the configuration through a ref in getContext.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): never run an employee on a disabled model

resolveModel() fell back to the employee's first listed model without
checking that it was still enabled, and the chat fell back to the global
list when none of the employee's models were, so display and run
diverged again. Pick only from the listed models listAllEnabledModels()
still offers, reject when none are, and give the chat no model in that
case instead of the global list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): name the agent failure in a chat's error event

The chat routes always answer 200 SSE, so the status map for agent
failures never reached a caller and the error event carried only the
message. Carry AgentServiceError.code in the error event: the streamed
path passes it from describeFailure(), and the non-streamed path keeps
the failure as the cause of the ResourceActionError for runSSEAction to
read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): refuse a permission for an MCP tool that is not connected

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): type a created conversation's sessionId as present

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): authorize a file preview before opening the file

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: stop registering MCP servers from a registrar

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(ai-employee): keep a fixed agent to the Skills it was given

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(ai-employee): cover preview access, past-conversation models and MCP permissions across a restart

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): correct the Skill and docs on surfaces, MCP, keys and unattended runs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): break the server import cycle through container tokens

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): resolve the server entry to source in a workspace

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): point the Skill at the plugin's working example pages

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): keep an employee's tools when a message carries a form

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(ai-employee): let createAgent() take its checkpointer

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(ai-employee): build a provider call's tools with an agent's tool context

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: remove AIChatContext fields LLMProvider never read

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(ai-employee)!: remove AIChatContext.systemPrompt and the unused AIChatContextOptions

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): merge the library Skill into the plugin Skill

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): register employees, tools and Skills only through the registrar

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(ai-employee): consolidate the branch's changesets into ten

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): create an App's config.yml with config:init in the Skill

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): keep a file's name in any script, and preview a sent image

An uploaded file kept only the ASCII characters of its name, so `客户截图.png` was stored as `.png` with no extension. History returned stored attachments with no `url` or `preview`, so after sending, an image rendered as a plain file.

The display name is now kept as given, minus any directory part and control characters, capped at 128 characters with the extension preserved; only the storage key is reduced to safe characters. History pages, including sub-agent messages, give every `aiFiles` attachment the preview address its upload returned, and the preview response sends the original name through RFC 6266 `filename*`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): enable attachments on every chat surface in the Skill

`enableAttachments` defaults to `false`, and the Skill enabled it only when a flow started from an uploaded file, so agents built chats that could not take a screenshot or a document. The Skill now sets it on every chat surface unless the user declines file uploads, the side-panel example carries it, and the storage disk is raised with the user when attachments are enabled rather than as a precondition.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): add a web search toggle to every chat surface in the Skill

The composer has no web search button, and `AIChatProvider.webSearch` defaults to `false`, so a chat built from the Skill never searched while the plugin's `chat` example showed a toggle that looked built in. The Skill now has the page add the toggle as a composer action, enabled only when `currentModel.supportWebSearch` is true and switched off when the user moves to a model that cannot search, with a sample checked against the Registry's types, and a completion check that observes it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(ai-employee): add enableWebSearch to the chat composer

The composer had no web search control, so every page that wanted one rebuilt the plugin example's composer action and its model check by hand. `AIChatWindow`, `AIChatCompact` and `ChatComposer` now take `enableWebSearch`, which defaults to `false` and puts a toggle beside the file action. The toggle is usable only when the selected model's `supportWebSearch` is true, and switches off when the user moves to a model that cannot search.

`AIChatProvider` holds the switch, starting from its `webSearch` prop, and `useAIChatBase()` exposes `webSearch` and `setWebSearch`; a chat that sets `webSearch` without mounting the toggle sends it as before. The `/dev/ai-components/chat` example uses the new prop, and the Skill sets it on every chat surface unless the user declines web search.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ai-employee): reapply overriding model lists on start, and report resumed tool call failures

`overrideEnabledModels` took effect only on a config reload. On start the configured services were synchronized into memory, and switching to the database kept every stored `enabledModels`, so after a restart a service still ran the list an administrator had curated. The synchronization now runs again against the stored rows once the switch is done, as a reload does, and a boot-level test covers it.

`resumeToolCall` reported a failed run with the top-level message and no `code`, unlike a send or a resend. It now describes the failure the same way, so its SSE error event carries the `AgentServiceErrorCode` and the root message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs(ai-employee): align the Skill with the code

An audit of every checkable claim in the AI Employee Skill against the code found samples that would not compile and descriptions that no longer matched. Among them: the readiness gate and provider samples passed `nocobaseAIService`, which bypasses the App's API client; page context reaches a message only through a trigger, task, shortcut or draft chip, not from the scope alone; the frontend tool sample destructured `unknown` arguments; history rows carry the employee's username as `role`, and work context items use `id`; `ai:listLLMServices` filters by capability and `ai:listModels` answers only for embeddings; `sendMessages` ignores `systemMessage` and `skillSettings`, and `resumeToolCall` ignores `toolCallIds`; tasks fill the composer unless `autoSend` is true; and the custom provider sample registered at module level with an unexported options object.

The readiness gate test's root stand-in now defaults its service the way the real root provider does, since the sample no longer passes one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 08:16:04 -04:00
PiEgg c5d628e14e docs: fold the AI Agent section into Get Started (#421) 2026-09-24 15:47:30 +08:00
chenos d5a18ff33c feat(templates): ship a Dockerfile with every application template (#416)
* feat(templates): build every application template into a Docker image from source

Each template now ships a Dockerfile and Dockerfile.dockerignore. The build
stage runs pnpm build on the builder's own architecture and cross-targets
native modules, so multi-platform images compile nothing under emulation. The
runtime stage holds only dist/ and config.example.yml, runs node directly as
the node user without pnpm, and reads /app/config.yml and /app/storage from
mounts. The same file builds a generated application from its root and a
template inside this repository from the repository root via APP_DIR.

The official Hub image is now built from the Hub template's Dockerfile at the
release tag, replacing Dockerfile.hub. This also fixes the published image
failing with EACCES on /app/storage when HUB_STORAGE_DIR is unset, which the
playground Compose file hit and the smoke test, setting it, never did.

app-tools now carries the application's registry lines into dist/.npmrc.
dist/ is its own pnpm root, so the production install ignored the registry
create-app records and failed with a 404 on any machine without it in user
configuration, including every container build.

* feat(templates): package a prebuilt dist into the Docker image with DIST=prebuilt

--build-arg DIST=prebuilt skips the build stage and packages the dist/ in the
build context, selected through FROM dist-${DIST} so BuildKit never runs the
unused stage. It works in a template's own directory, where a source build
cannot resolve workspace dependencies.

The stage refuses a dist/ whose buildTarget is not linux, glibc, the target
architecture and the image's Node major, and one whose client was built for a
different APP_BASE_PATH, since either builds an image that fails only at
runtime. The ignore file re-includes dist/ last so dependency directories
named like excluded paths survive, then excludes dist/.env, which pnpm build
fills from local .env files. A source build without a lockfile in the context
now says how to build instead of failing inside pnpm.

* fix(templates): point a prebuilt dist built for another architecture at --platform

A dist/ built with --target linux-x64 on Apple silicon was rejected with advice
to rebuild for arm64, when the usual intent is an amd64 image for an amd64
server. The check now names the --platform flag that matches the dist/, and the
documented examples pass --platform alongside --target.
2026-09-24 12:47:30 +08:00
PiEgg b70db9e291 docs: fold deployment and reference into the Development section (#414) 2026-09-24 09:53:43 +08:00
chenos d7543b56a8 feat(authentication): support users.initialAdmin.email (#411)
Let the initial administrator seed read an optional email, defaulting to
admin@nocobase.com, validate it and store it lowercase. Spell out every
initialAdmin field in the three templates' config.example.yml and update
the related docs and Skills.
2026-09-24 09:38:26 +08:00
KatherineandClaude Opus 5 6538c4af9d docs(deployment): say which environment a build target must match (#410)
The publishing page told readers to match the Hub host with --target and
--node-version without saying how to learn what that host is, so the flags
could only be inferred from whichever values happened to work last time. A
mismatch is accepted at upload and at deploy, and surfaces only when the
application starts and a native module fails to load.

State the values instead of the instruction to find them. A Docker
deployment runs Hub inside an image based on Debian bookworm with Node 24,
so its build flags are fixed and the host's own Node version and libc do
not apply; a template deployment reads them on the server that runs Hub.
Record that a Hub upgrade changing the Node major version changes these
flags, and add the ABI failure to the troubleshooting tables so the error
text leads back to the cause.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-24 08:41:20 +08:00
chenos 979c8390c5 docs(skills): tighten the create-app Skill and its agent guide (#403)
* docs(skills): cover Hub creation and soften the new-session handover

The create-app Skill now passes --template=hub for a Hub and starts it with
the nextCommands create-app returns, clarifies that nextCommands default to
SQLite, and recommends rather than requires a new session, with a fallback
for continuing in the current one.

The agent guide the Skill links to no longer tells agents to run
pnpm config set @nocobase:registry, which the Skill forbids.

* docs(skills): stop and explain clearly when Node.js or pnpm is missing

The previous change dropped "before continuing", so an agent could report a
missing toolchain and carry on. It now stops before creating anything and
tells the user the found and required versions, the install command for
their system, and to open a new shell, then checks again.

* docs(skills): say exactly how to start the new session in the application

Distinguish an application created in the session's own directory, where
restarting there is enough, from one elsewhere, which needs its path and
the agent's start command, and cover desktop clients in the Skill.

* docs(skills): suggest a first request and keep the handover short

Finish now tells the user what to ask for in the new session, with a
small example and the first-feature guide, and asks for a concise
handover that names only the session case that applies.

* docs(skills): keep the service and database choice from surprising users

- Say the agent-started service stops with the session, and how to start it
  again.
- Use a database the user already named instead of asking again.
- After retrying a failed install, continue with Configure using the known
  commands, since the retry result no longer carries them.
- Require WSL on Windows, where the POSIX command form does not work.
- Allow a long timeout for a Hub build, wait for the Local: line of pnpm dev,
  and describe AGENTS.md as possibly unloaded rather than never loaded.

* docs(skills): leave the Hub out of the create-app Skill for now

The Skill and its guides describe only the default template until the Hub
flow is ready to document.
2026-09-23 21:31:28 +08:00
Zeke ZhangandClaude Opus 5.5 1124eebb37 revert(templates): remove the Settings theme page and 30 theme presets (#398)
This reverts the theme parts of commit d6967003ef (#360).

Theme selection returns to the header's Appearance popover and Settings → Theme is removed, together with the 30 converted presets and the single-density default. The surface-token corrections #360 made in plugin and example pages are kept, as is the guidance in the app-skills references that surface and outline tokens name a layer rather than a shade. The changesets #360 added stay in .changeset/pre/ because the release tooling has already consumed them; a new changeset records the revert. The route tests written after #360 keep their current shape and only drop the Settings theme page grant.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:07:28 +08:00
chenos 4e58fe3208 feat(cli): configure applications with config:init, config:set and config:check instead of at creation (#396)
Creation no longer writes config.yml or chooses a database. It produces a
project that is ready to configure, and three commands in @nocobase/app-cli
configure it: config:init writes the file, config:set changes it, and
config:check verifies it.

- create-app drops --dialect, writes an .npmrc recording a non-public
  registry, and returns the remaining procedure as nextCommands.
- config:init writes config.yml from config.example.yml with generated
  secrets, installs nothing, reports a missing driver with the pnpm add that
  supplies it, reports an already configured application as unchanged, and
  works inside dist/.
- config:check loads the configuration without starting the application,
  reports missing or placeholder secrets, an ephemeral session secret,
  unknown sections and unexpanded references, and connects to every
  database other than SQLite.
- config:set edits the file in place, keeping comments, and reads secrets
  with --from-env.
- pnpm dev and pnpm start stop on an unconfigured application and name
  pnpm config:init.
- @nocobase/app-plugin-install and install mode are removed.
- The templates declare @nocobase/db-sqlite, and Examples drops its unused
  MySQL, Oracle and PostgreSQL drivers.
- A missing driver is reported for every connection at once.

Supersedes #385.
2026-09-23 18:19:39 +08:00
KatherineandClaude Opus 5 571e54e513 docs(hub): document how to create a publishing API key (#395)
* docs(hub): document where a publishing key comes from

The Default template README, the application development Skill and the
Hub publishing page all named HUB_API_KEY as a required variable without
saying where it is created. Publishing for the first time meant leaving
the deployment instructions to find the entry point and the two required
actions in @nocobase/app-plugin-hub's README, and a key granted without
`deploy` only surfaced later as a 403 during upload.

Document the API Keys page, its `hub.app / manage-api-keys` requirement,
the application binding and the Upload release / Deploy release choice in
all three places, using the labels the UI actually shows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(hub): illustrate the publishing key steps

Add the API Keys page and the Create API Key dialog to the Chinese
publishing page, and note that permissions stay disabled until an
application is selected. Name both permissions in English alongside the
translated labels, because the screenshots show the English UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(hub): mirror the publishing key steps on the English page

The English page carried the key guidance as one paragraph while the
Chinese page had step-by-step subsections and screenshots. Give both the
same four subsections under the CLI section, the same numbered steps, and
the same two screenshots.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 16:10:39 +08:00
Jiannx ffafc2a980 feat(notification): add named notification channels and explicit navigation targets (#331)
* fix(db): preserve undefined temporal query bindings

* feat(notification): add named channels and explicit navigation targets

* feat(notification): simplify channel message configuration

* fix(notification): align send API and channel manager tests

* feat(notification): simplify channels and delivery retries

* docs(notification): clarify send parameters

* feat(notification): add task assignment example

* fix(notification): stabilize inbox component test
2026-09-23 08:24:30 +08:00
KatherineandClaude Fable 5.1 2ae6b2b722 docs: ship the deployment Skill with applications and finish the deployment guide (#376)
* docs: ship the deployment Skill with app-skills and close the deployment guide gaps

Move `nocobase-deployment` from the repository-only `.agents/skills/` into
`@nocobase/app-skills`, so `nocobase skills sync` delivers it to every
generated application; the repository keeps a relative symbolic link there
and the Claude mirror follows it. Point each template's AGENTS.md at the
Skill, explain in the READMEs and the Skill that `pnpm build` already runs
`pnpm install --prod` inside `dist/`, tell the workflow runtime's missing
Artifact error how to enable the deployed hash, and fix the tutorial's
archive path and start command.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: write the English deployment overview, configuration, standalone and Docker pages

The four pages were "Being written" placeholders while their Chinese
counterparts were complete. Each now carries the same content as the
Chinese page: the deployment modes and how Hub relates to applications,
the shared, standalone and Hub-hosted configuration with the database,
secret, address and reverse proxy details, the build-to-systemd flow with
its acceptance checks, and the Docker image, Compose and update steps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 18:19:16 +08:00
PiEgg 944cf3c1e4 ci: release as nocobase bot and skip generated merge checks (#378) 2026-09-22 17:44:00 +08:00
PiEgg 97ad17717d ci: select Pro follow-up per release dispatch (#377) 2026-09-22 17:17:12 +08:00
PiEgg e730e03a46 ci: host commercial release workflows in the OSS repository (#374) 2026-09-22 16:30:39 +08:00
Zeke ZhangandClaude Opus 5 d6967003ef feat(templates): add a Settings theme page and 30 theme presets (#360)
* feat(templates): move theme selection to Settings

The header's Appearance popover carried two decisions of different weight. Light and dark is an everyday switch, so it becomes one icon button that flips to the resolved mode to the opposite explicit one and states what it does through its tooltip and accessible label. Choosing a theme is longer-lived and moves to Settings → Theme, which renders every registered preset as a preview card in an auto-filling grid, marks the selection, and adds a search field once the registry holds more than eight entries — so an application with dozens of themes stays workable.

The page declares authz on its route, so administrators hold it by default and can grant it to another role in the permissions interface, exactly like User management. Selection stays browser-local under the same storage keys, cross-tab sync and config.yml defaults: no server code, migration or seed. system remains a valid configured default, because it is the header button that leaves it, not the page.

The appearance locale block now carries toggle and theme.{title,description,search,empty} in place of title, mode, preset, light, dark and system, and appearance.themes is unchanged. The themes and header-action references, the Chinese theme page, and the Default template scope line describe the new arrangement.

* fix(templates): always show the theme search field

The theme page rendered its search field only once the registry passed eight presets, which left it invisible in every application that ships the two built-in themes — the state a reader sees first, and the reason it reads as missing. The field is now part of the page header unconditionally: typing filters the cards by localized name or ID, a short registry included, and clearing restores the grid.

Nothing else moves: the query still trims and matches case-insensitively, the empty state still names what was not found, and the stored preference, storage keys and defaults are untouched.

The theme-page test now drives the short registry — the field is present, filters to the matching card, reports the empty state and restores the grid when cleared — and the themes reference and Chinese theme page no longer describe a threshold.

* feat(templates): add 30 converted theme presets

Convert 30 palettes from the tweakcn collection into application theme
files for the three templates. Each file states the same tokens as the
shipped presets and takes only the colours and corner radius from
upstream, so a converted preset keeps this application's typography,
spacing and elevation and needs no font resources.

Record the source revision, the licence and the dropped values in
client/theme/themes/THIRD-PARTY-NOTICES.md, cover the registry and its
labels with tests, hold every preset to the shared dimensions and to a
palette of its own, and document the conversion rules in the theme
reference and the application documentation.

* feat(templates): make the compact density the only one and name it Default

Remove the Spacious preset, rename Compact to `default` so it owns the bare
root selectors and the fallback, and move every converted preset onto the
compact `--spacing` and line heights while keeping its own radius. Update the
theme tests, the app-skills theme references and the documentation site to
match, with a changeset covering the three templates and app-skills.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(theme): keep bubblegum's surface and outline tokens in their roles

The preset was carried over from tweakcn verbatim, and upstream spends the
generic surface and outline roles on decoration: --card was a cream 101 degrees
of hue from the pink --background, --border was --primary itself at chroma 0.18
against a median of 0.02 across the other thirty presets, and --muted was a
cyan. One demonstration card and a few dividers carry that; a settings page
stacking several panels over dozens of hairlines does not, and pages showed
pink, cream, cyan and teal at once.

Retune six light values -- --card, --border, --muted, --input, --sidebar-border
and --sidebar-primary -- so those roles stay in the background's hue family and
the preset's colour stays in --primary, --secondary and --accent. The dark
values, the radius and every other preset are unchanged. All three templates
carry the same file, and THIRD-PARTY-NOTICES.md records the deviation.

The same pages also used tokens for something other than their role, which no
neutral preset makes visible. Authorization's two page shells and four Hub pages
painted the whole page with bg-muted/20, which is the page surface and belongs
to bg-background. The AI employee page's read-only fields hand-rolled
bg-muted/40 instead of the shared Input and Textarea with disabled, three
information callouts were fixed bg-blue-50, and the MCP transport labels were
fixed bg-blue-100/bg-green-100/bg-amber-100; the transports now take their three
tones from the theme's chart series, which is what a preset defines to be told
apart.

Correct three fixed colours while they are in hand. The missing-knowledge-base
warning and the schedule detail page's target-issue icon named a light-mode ink
with no dark counterpart and were close to unreadable on a dark card; the routes
example reported a load failure in a fixed red, which is what --destructive is
for.

State the rule in the theme authoring and token references, so a preset
converted tomorrow is checked against it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(settings): give every surface the token that matches what it is

The permission set editor is where this shows: its two tabs sit in one panel,
and the permission configuration tab painted itself bg-background while the
assignment tab inherited the panel's bg-card, so switching tabs changed the page
colour under the same heading. The same mistake is spread across the settings
pages, and none of it is visible under a preset whose page and card are
near-identical.

Each token names a layer rather than a shade, and every site now uses the one
that describes it. A panel resting on the page is bg-card, which is what the AI
tools and skills pages already used while the LLM service, MCP service,
conversation, API key, user and notification log panels named the page surface
instead -- two lists in one plugin, one framed and one flat. A dialog or drawer
is bg-popover, which is what the shared Sheet, Dialog and Popover primitives use
and what six hand-rolled drawers and dialogs did not. An opaque sticky header,
footer or table head names the surface it scrolls within rather than the page
behind it. A form control names no surface at all and inherits the one it sits
on, the way the shared Input and Textarea do with bg-transparent; twenty
hand-rolled inputs, selects and text areas were pinned to the page colour and
showed through as a differently coloured box inside every card.

The service table layout test pinned bg-background on the panel it covers, which
is the value being corrected, so it now expects bg-card.

State in the styling reference which token describes which layer, and why
picking one because it happens to look right is what puts a page-coloured block
inside a panel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 16:07:10 +08:00
Charles Wu c04406a37c docs: point the application creation prompt to the Markdown guide (#371)
The docs site publishes every page as Markdown, so the prompt now links to create-app-with-agent.md and asks the agent to read the linked file directly. This keeps the prompt in the same shape as the CLI quick-start prompt instead of handing the agent an HTML page.

The Chinese and English prompts are updated together.
2026-09-22 13:08:01 +08:00
Charles Wu 30ae103b03 docs: require a new session to load application Skills after creation (#353)
Creating an application synchronizes its development Skills into .agents/skills/, but AI Agents load Skills when a session starts. The getting-started page now tells readers to start a new session in the application directory before continuing development, and explains why. The other-directory tip is folded into this reminder.

The agent guide no longer tells the Agent not to request a new session; it now hands over after startup. The first-feature page and both page descriptions follow the same flow. Chinese and English are updated together.
2026-09-22 11:57:58 +08:00