feat(agent-skills): manage app agent skills from the CLI - #575
feat(agent-skills): manage app agent skills from the CLI#575yardend-wix wants to merge 18 commits into
Conversation
Export SKILL_NAME_REGEX, AgentSkillApiResponseSchema, ListAgentSkillsResponseSchema, and SyncAgentSkillsResultSchema (and their inferred types) so they can be used by Task 2 and other modules. These are part of the public API of the agent-skill module. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds selected_skill_names as a typed, defaulted array on AgentConfigSchema. selected_workspace_skill_ids remains untyped passthrough via looseObject. Updates agents.spec.ts fixtures/assertions to account for the new default field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `base44 agent-skills pull|push`, registers them in program.ts, and adds TestAPIServer mocks for the agent-skills reconcile endpoints. Also fixes deploy/env-token-auth tests that were missing a GET agent-skills mock now that deploy always reconciles agent skills (pushAgentSkills fetches remote unconditionally, even for an empty local set).
…mote skills pushAgentSkills now short-circuits on an empty list (mirrors pushAgents), so 'base44 deploy' from a project with no local skills no longer reconciles against remote and deletes them. Reverts the deploy-spec GET mocks that had masked the missing guard, and adds a unit test asserting the empty push makes no HTTP calls.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/cli@0.1.6-pr.575.77cce10Prefer not to change any import paths? Install using npm alias so your code still imports npm i "base44@npm:@base44-preview/cli@0.1.6-pr.575.77cce10"Or add it to your {
"dependencies": {
"base44": "npm:@base44-preview/cli@0.1.6-pr.575.77cce10"
}
}
Preview published to npm registry — try new features instantly! |
1cff89f to
3594d8c
Compare
…d conventions - add 'successfully' to task success messages (matches agents/connectors/entities) - 'Manage project agent skills' (matches the 'project' wording used by siblings) - add '(replaces all remote/local agent skills)' clauses to push/pull descriptions - drop the dead AgentSkillApiResponse type alias
# Conflicts: # docs/resources.md
#573) Main added confirmPush + -y/--yes to the resource push commands; mirror it on agent-skills push so it prompts (and requires --yes non-interactively) like agents.
4a86148 to
2e3a871
Compare
- add agent-skills_pull.spec.ts (mirrors agents_pull: written/skip/update/delete/error) - add the non-interactive '--yes required' push test (parity with agents/entities/connectors) - make the push update path real (was a dead mock) and assert Created/Updated/Deleted - move readProjectConfig skills case into project.spec.ts; add 'throws on invalid agent skill file' + invalid-agent-skill fixture - add core schema-validation negatives (bad name, empty body) to config spec - add missing .app.jsonc to the with-agent-skills fixture (testing rule #3)
| mockDeploySite: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("../../src/core/resources/agent-skill/index.js", () => ({ |
There was a problem hiding this comment.
hmmm I don't think we have tests that are mocking source files up until now, and I dont think we want to start introducing that, we have our integreations tests that make requests to mock http servers so we dont have to do these mocks
| it("throws on invalid agent skill file", async () => { | ||
| await expect( | ||
| readProjectConfig(resolve(FIXTURES_DIR, "invalid-agent-skill")), | ||
| ).rejects.toThrow(); |
There was a problem hiding this comment.
make it assert on the error message as well? so we can see what the user will get when reading the spec file
| const del = vi.fn(); | ||
| const get = vi.fn(); | ||
|
|
||
| vi.mock("../../src/core/clients/index.js", () => ({ |
There was a problem hiding this comment.
same here, not sure we want src mocking tests
| import { AgentSkillSchema } from "./schema.js"; | ||
|
|
||
| function parseSkillFile(raw: string): { description: string; body: string } { | ||
| const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); |
There was a problem hiding this comment.
there's probably a lib that can parse frontmatter files nope?
| } | ||
| } | ||
| } catch (error) { | ||
| throw await ApiError.fromHttpError(error, "syncing agent skills"); |
There was a problem hiding this comment.
@claude
why do we have await on creating an error object? what async work are we doing there?
(Yarden - this is just me interested, not a review comment)
There was a problem hiding this comment.
Claude finished @netanelgilad's task in 48s —— View job
Why await ApiError.fromHttpError(...)?
Short answer: the await isn't on the constructor — it's on a static async factory, and the async work is reading the HTTP response body.
ApiError's constructor is fully synchronous. But fromHttpError (packages/cli/src/core/errors.ts:341) needs the server's error payload to build a useful message, and with ky's HTTPError the body is an unconsumed Response stream:
static async fromHttpError(error: unknown, context: string): Promise<ApiError> {
if (error instanceof HTTPError) {
...
responseBody = await error.response.clone().json(); // ← the only async bit
const parsed = ApiErrorResponseSchema.safeParse(responseBody);
message = formatApiError(responseBody, parsedData);
hints = ApiError.getReasonHints(parsedData);
details = parseErrorDetails(parsedData.extra_data);
...Response.json() returns a promise (body is a stream that may not have arrived yet), so everything downstream of it — the parsed backend message, the hints, the details — has to be awaited before the error object can be constructed. Everything else in that method (status normalization, X-Request-ID header, request URL/method) is sync; the body read is what forces async.
Two side notes:
.clone()is there because aResponsebody can only be read once — cloning leaves the original consumable by anything else that handles the error.throw await f()parses asthrow (await f())—awaitbinds tighter thanthrow. That matters: drop theawaitand you'dthrowa Promise, so everycatch (error)downstream would receive a pending promise instead of anApiError, anderror instanceof ApiErrorchecks would silently fail.
So the pattern is "async construction because we need I/O to build the message", which is why it's a static factory rather than work hidden in a constructor (constructors can't be async).
This is the established convention across the codebase — ~34 call sites in core/**/api.ts use it, including the two new ones in agent-skill/api.ts (lines 17 and 68), so this PR is just following it.
| Branch: feat/agent-skills-cli
Note
Description
Adds agent skills as a first-class Base44 CLI resource. Agent skills are app-scoped instruction snippets shared across an app's agents, stored as one markdown file per skill under
base44/agent-skills/(filename = skill name, frontmatterdescription= summary, body = instruction text). This PR adds the schema, file I/O, a reconciling API layer,base44 agent-skills pull/pushcommands, and wires skills intoreadProjectConfig()and unifieddeploy— pushed before agents, so an agent'sselected_skill_namesalways resolves against skills that already exist remotely.Related Issue
None
Type of Change
Changes Made
New
agent-skillresource (packages/cli/src/core/resources/agent-skill/)schema.ts—AgentSkillSchema(lowercase-hyphenatedname≤64,description≤1024,body≤15000), plusListAgentSkillsResponseSchemaandSyncAgentSkillsResultSchema.config.ts—readAllAgentSkills()/writeAgentSkills(): a minimalkey: valuefrontmatter reader/writer (no YAML dependency, descriptions are single-line) and content diffing so unchanged files are not rewritten.api.ts—fetchAgentSkills()andpushAgentSkills(), which reconciles against remote: POST creates, PUT updates only whendescription/bodychanged, DELETE removes remote skills absent locally. An empty local list returns early with no HTTP calls, so a skill-less project can never wipe remote skills viadeploy.resource.ts—agentSkillResourceimplementing the standardResource<AgentSkill>interface.New CLI commands (
base44 agent-skills)agent-skills pull— fetches remote skills and syncs local.mdfiles, reporting written/deleted names, or "already up to date".agent-skills push— reconciles local skills to remote behind the sharedconfirmPush()guard with-y, --yes, matching theagentspush conventions.agent-skillscommand group inprogram.ts.Project config & deploy
agentSkillsDirproject setting (defaultagent-skills);agentSkillsadded toProjectResources/ProjectDataand read in parallel with the other resources.hasResourcesToDeploy()now accounts for skills;deployAll()pushes skills at step 3, before agents. Plugin and projectless config paths returnagentSkills: [].Agents
selected_skill_namesis now a typed, defaulted ([]) field onAgentConfigSchemainstead of relying onlooseObjectpassthrough.selected_workspace_skill_ids(org-shared workspace skills) still passes through untouched.Template & docs
backend-and-clienttemplate ships an exampleweekly-reportskill, referenced fromtask_manager.jsoncviaselected_skill_names.docs/resources.mdgains an "Agent skills" section, updated keywords, and the corrected deploy order.Tests
agent-skills_api.spec.ts(reconcile create/update/delete, empty-push guard),agent-skills_config.spec.ts(parse, round-trip, delete, missing dir, invalid name, empty body),agent-skills_deploy.spec.ts(skills-before-agents ordering,hasResourcesToDeploy).agent-skills_pull.spec.tsandagent-skills_push.spec.ts(success, no-op, write/update/delete on disk, API error, non-interactive without--yes).TestAPIServergainsmockAgentSkillsFetch/Create/Update/Delete/FetchError; newwith-agent-skillsandinvalid-agent-skillfixtures;project.spec.tscovers reading skills and rejecting an invalid skill file; existing agent specs updated for the defaultedselected_skill_names.Testing
npm test)Checklist
docs/(AGENTS.md) if I made architectural changesAdditional Notes
agents(feat(push): confirm before destructive resource push #573), soagent-skills pushrefuses to run non-interactively without--yes.pushAgentSkills([])short-circuits, the messageagent-skills pushlogs for an empty project ("No local agent skills found - this will delete all remote skills", asserted inagent-skills_push.spec.ts) overstates what actually happens — nothing is deleted. Worth reconciling the wording.descriptionvalues are supported; multi-line or block-scalar frontmatter yields an empty description and fails Zod validation.typecheckwere not executed in this environment (this runner is not permitted to invoke them), so the "tested locally" / "all tests pass" boxes are left unchecked rather than asserted.🤖 Generated by Claude | 2026-07-28 10:59 UTC | 185208d