Skip to content

feat(agent-skills): manage app agent skills from the CLI - #575

Open
yardend-wix wants to merge 18 commits into
mainfrom
feat/agent-skills-cli
Open

feat(agent-skills): manage app agent skills from the CLI#575
yardend-wix wants to merge 18 commits into
mainfrom
feat/agent-skills-cli

Conversation

@yardend-wix

@yardend-wix yardend-wix commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

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, frontmatter description = summary, body = instruction text). This PR adds the schema, file I/O, a reconciling API layer, base44 agent-skills pull / push commands, and wires skills into readProjectConfig() and unified deploy — pushed before agents, so an agent's selected_skill_names always resolves against skills that already exist remotely.

Related Issue

None

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Other (please describe):

Changes Made

New agent-skill resource (packages/cli/src/core/resources/agent-skill/)

  • schema.tsAgentSkillSchema (lowercase-hyphenated name ≤64, description ≤1024, body ≤15000), plus ListAgentSkillsResponseSchema and SyncAgentSkillsResultSchema.
  • config.tsreadAllAgentSkills() / writeAgentSkills(): a minimal key: value frontmatter reader/writer (no YAML dependency, descriptions are single-line) and content diffing so unchanged files are not rewritten.
  • api.tsfetchAgentSkills() and pushAgentSkills(), which reconciles against remote: POST creates, PUT updates only when description/body changed, 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 via deploy.
  • resource.tsagentSkillResource implementing the standard Resource<AgentSkill> interface.

New CLI commands (base44 agent-skills)

  • agent-skills pull — fetches remote skills and syncs local .md files, reporting written/deleted names, or "already up to date".
  • agent-skills push — reconciles local skills to remote behind the shared confirmPush() guard with -y, --yes, matching the agents push conventions.
  • Both registered under a new agent-skills command group in program.ts.

Project config & deploy

  • New agentSkillsDir project setting (default agent-skills); agentSkills added to ProjectResources/ProjectData and 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 return agentSkills: [].

Agents

  • selected_skill_names is now a typed, defaulted ([]) field on AgentConfigSchema instead of relying on looseObject passthrough. selected_workspace_skill_ids (org-shared workspace skills) still passes through untouched.

Template & docs

  • The backend-and-client template ships an example weekly-report skill, referenced from task_manager.jsonc via selected_skill_names.
  • docs/resources.md gains an "Agent skills" section, updated keywords, and the corrected deploy order.

Tests

  • New core specs: 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).
  • New CLI specs: agent-skills_pull.spec.ts and agent-skills_push.spec.ts (success, no-op, write/update/delete on disk, API error, non-interactive without --yes).
  • TestAPIServer gains mockAgentSkillsFetch/Create/Update/Delete/FetchError; new with-agent-skills and invalid-agent-skill fixtures; project.spec.ts covers reading skills and rejecting an invalid skill file; existing agent specs updated for the defaulted selected_skill_names.

Testing

  • I have tested these changes locally
  • I have added/updated tests as needed
  • All tests pass (npm test)

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (if applicable)
  • My changes generate no new warnings
  • I have updated docs/ (AGENTS.md) if I made architectural changes

Additional Notes

  • The confirmation guard mirrors the destructive-push parity work done for agents (feat(push): confirm before destructive resource push #573), so agent-skills push refuses to run non-interactively without --yes.
  • Because pushAgentSkills([]) short-circuits, the message agent-skills push logs for an empty project ("No local agent skills found - this will delete all remote skills", asserted in agent-skills_push.spec.ts) overstates what actually happens — nothing is deleted. Worth reconciling the wording.
  • Skill files are parsed with a hand-rolled frontmatter regex rather than a YAML parser, so only single-line scalar description values are supported; multi-line or block-scalar frontmatter yields an empty description and fails Zod validation.
  • Tests and typecheck were 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

yardend-wix and others added 10 commits July 15, 2026 18:33
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>
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

🚀 Package Preview Available!


Install this PR's preview build with npm:

npm i @base44-preview/cli@0.1.6-pr.575.77cce10

Prefer not to change any import paths? Install using npm alias so your code still imports base44:

npm i "base44@npm:@base44-preview/cli@0.1.6-pr.575.77cce10"

Or add it to your package.json dependencies:

{
  "dependencies": {
    "base44": "npm:@base44-preview/cli@0.1.6-pr.575.77cce10"
  }
}

Preview published to npm registry — try new features instantly!

@yardend-wix
yardend-wix force-pushed the feat/agent-skills-cli branch from 1cff89f to 3594d8c Compare July 26, 2026 07:05
…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
#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.
@yardend-wix
yardend-wix force-pushed the feat/agent-skills-cli branch from 4a86148 to 2e3a871 Compare July 28, 2026 10:19
- 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", () => ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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", () => ({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]*)$/);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's probably a lib that can parse frontmatter files nope?

}
}
} catch (error) {
throw await ApiError.fromHttpError(error, "syncing agent skills");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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)

@claude claude Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 a Response body can only be read once — cloning leaves the original consumable by anything else that handles the error.
  • throw await f() parses as throw (await f())await binds tighter than throw. That matters: drop the await and you'd throw a Promise, so every catch (error) downstream would receive a pending promise instead of an ApiError, and error instanceof ApiError checks 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants