Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
14c6376
feat(agent-skills): add skill schema and markdown file I/O
yardend-wix Jul 15, 2026
89e3754
fix(agent-skills): export skill API schemas and name regex
yardend-wix Jul 15, 2026
ef3bb3e
feat(agent-skills): add reconcile push API and resource
yardend-wix Jul 15, 2026
8cfae69
fix(agent-skills): export fetchAgentSkills and skill schemas
yardend-wix Jul 15, 2026
1c2cf11
feat(agents): make selected_skill_names a first-class field
yardend-wix Jul 15, 2026
766b97c
feat(agent-skills): load skills into project config
yardend-wix Jul 15, 2026
e0cb3ea
feat(agent-skills): deploy skills before agents
yardend-wix Jul 15, 2026
89f00c8
feat(agent-skills): add pull/push commands
yardend-wix Jul 15, 2026
c64b65f
fix(agent-skills): guard empty push to prevent deploy deleting all re…
yardend-wix Jul 26, 2026
3594d8c
feat(agent-skills): scaffold example skill and document resource
yardend-wix Jul 26, 2026
97dccae
docs(agent-skills): trim resources.md section to match contributor-do…
yardend-wix Jul 28, 2026
9061da2
refactor(agent-skills): align user-facing messages with agents comman…
yardend-wix Jul 28, 2026
6d1194a
chore(agent-skills): drop ponytail comment on reconcile push
yardend-wix Jul 28, 2026
925c0c4
Merge remote-tracking branch 'origin/main' into feat/agent-skills-cli
yardend-wix Jul 28, 2026
2e3a871
feat(agent-skills): confirm before destructive push (parity with agen…
yardend-wix Jul 28, 2026
915bed4
docs(agent-skills): drop the behavior-details paragraph from resource…
yardend-wix Jul 28, 2026
acbb069
chore(agent-skills): remove ponytail comment from frontmatter parser
yardend-wix Jul 28, 2026
185208d
test(agent-skills): close coverage gaps and align with test conventions
yardend-wix Jul 28, 2026
fa71ed6
refactor(agent-skills): address review — use front-matter lib, drop s…
yardend-wix Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions docs/resources.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Working with Resources

**Keywords:** resource, entity, function, agent, connector, push, readAll, deploy, site, tar.gz, deployAll, ProjectData
**Keywords:** resource, entity, function, agent, agent skill, connector, push, readAll, deploy, site, tar.gz, deployAll, ProjectData

Resources are project-specific collections (entities, functions, agents, connectors) that can be read from the filesystem and pushed to the Base44 API.
Resources are project-specific collections (entities, functions, agents, agent skills, connectors) that can be read from the filesystem and pushed to the Base44 API.

## Resource Interface

Expand Down Expand Up @@ -70,6 +70,10 @@ Deploy ships file contents verbatim — the source is never parsed or linted —

Entry files may also import `secrets` and `waitUntil` from `base44:runtime`. Locally, `base44 dev` runs functions on workerd via Miniflare by default — each function is bundled with esbuild + `@deno/loader` (`src/cli/dev/dev-server/function-bundler.ts`), with `base44:runtime` served as a virtual module, secrets as real Worker env bindings and `waitUntil` riding `ctx.waitUntil`. A fallback runtime covers installations where workerd is unavailable (compiled binaries, `B44_DEV_FUNCTIONS_RUNTIME=deno`) and supplies `base44:runtime` via an import map. A project-level `deno.json` import map is not applied to functions — locally or deployed — since only files under `base44/` are uploaded. See [`packages/cli/backend-runtime/README.md`](../packages/cli/backend-runtime/README.md) for the local implementation and its intentional differences from production.

## Agent skills

Agent skills are app-scoped instruction snippets shared across the app's agents. Unlike other resources they are stored as one markdown file per skill under the agent-skills directory (`base44/agent-skills/`, or `agentSkillsDir` in `config.jsonc`): the filename (without `.md`) is the skill name, the frontmatter `description` is the summary, and the body is the instruction text. Agents reference skills by name via `selected_skill_names`; `selected_workspace_skill_ids` (org-shared workspace skills) is not managed here and is passed through pull/push/deploy untouched.

## Site Module (Not a Resource)

The site module at `packages/cli/src/core/site/` handles deploying built frontend files. It follows a different pattern than resources:
Expand Down Expand Up @@ -109,9 +113,10 @@ const { appUrl } = await deployAll(projectData);
What it deploys (in order):
1. Entities (via `entityResource.push()`)
2. Functions (via `functionResource.push()`)
3. Agents (via `agentResource.push()`)
4. Connectors (via `pushConnectors()`) -- may return OAuth redirect URLs
5. Site (if `site.outputDirectory` is configured)
3. Agent skills (via `agentSkillResource.push()`)
4. Agents (via `agentResource.push()`)
5. Connectors (via `pushConnectors()`) -- may return OAuth redirect URLs
6. Site (if `site.outputDirectory` is configured)

```bash
base44 deploy # With confirmation prompt
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/cli/commands/agent-skills/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Command } from "commander";
import { getAgentSkillsPullCommand } from "./pull.js";
import { getAgentSkillsPushCommand } from "./push.js";

export function getAgentSkillsCommand(): Command {
return new Command("agent-skills")
.description("Manage project agent skills")
.addCommand(getAgentSkillsPushCommand())
.addCommand(getAgentSkillsPullCommand());
}
50 changes: 50 additions & 0 deletions packages/cli/src/cli/commands/agent-skills/pull.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { dirname, join } from "node:path";
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command } from "@/cli/utils/index.js";
import { readProjectConfig } from "@/core/index.js";
import {
fetchAgentSkills,
writeAgentSkills,
} from "@/core/resources/agent-skill/index.js";

async function pullAction({
log,
runTask,
}: CLIContext): Promise<RunCommandResult> {
const { project } = await readProjectConfig();
const dir = join(dirname(project.configPath), project.agentSkillsDir);

const remote = await runTask(
"Fetching agent skills from Base44",
() => fetchAgentSkills(),
{
successMessage: "Agent skills fetched successfully",
errorMessage: "Failed to fetch agent skills",
},
);

const { written, deleted } = await runTask(
"Syncing skill files",
() => writeAgentSkills(dir, remote.items),
{
successMessage: "Skill files synced successfully",
errorMessage: "Failed to sync skill files",
},
);

if (written.length > 0) log.success(`Written: ${written.join(", ")}`);
if (deleted.length > 0) log.warn(`Deleted: ${deleted.join(", ")}`);
if (written.length === 0 && deleted.length === 0)
log.info("All skills are already up to date");

return { outroMessage: `Pulled ${remote.total} agent skills to ${dir}` };
}

export function getAgentSkillsPullCommand(): Command {
return new Base44Command("pull")
.description(
"Pull agent skills from Base44 to local files (replaces all local agent skills)",
)
.action(pullAction);
}
60 changes: 60 additions & 0 deletions packages/cli/src/cli/commands/agent-skills/push.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Command } from "commander";
import type { CLIContext, RunCommandResult } from "@/cli/types.js";
import { Base44Command, confirmPush } from "@/cli/utils/index.js";
import { readProjectConfig } from "@/core/index.js";
import { pushAgentSkills } from "@/core/resources/agent-skill/index.js";

interface PushOptions {
yes?: boolean;
}

async function pushAction(
{ isNonInteractive, log, runTask }: CLIContext,
options: PushOptions,
): Promise<RunCommandResult> {
const { agentSkills } = await readProjectConfig();

log.info(
agentSkills.length === 0
? "No local agent skills found - this will delete all remote skills"
: `Found ${agentSkills.length} agent skills to push`,
);

const proceed = await confirmPush({
isNonInteractive,
yes: options.yes,
log,
warning:
"This will replace all remote agent skills with your local skills and delete any not present locally.",
});
if (!proceed) {
return { outroMessage: "Push cancelled" };
}

const result = await runTask(
"Pushing agent skills to Base44",
() => pushAgentSkills(agentSkills),
{
successMessage: "Agent skills pushed successfully",
errorMessage: "Failed to push agent skills",
},
);

if (result.created.length > 0)
log.success(`Created: ${result.created.join(", ")}`);
if (result.updated.length > 0)
log.success(`Updated: ${result.updated.join(", ")}`);
if (result.deleted.length > 0)
log.warn(`Deleted: ${result.deleted.join(", ")}`);

return { outroMessage: "Agent skills pushed to Base44" };
}

export function getAgentSkillsPushCommand(): Command {
return new Base44Command("push")
.description(
"Push local agent skills to Base44 (replaces all remote agent skills)",
)
.option("-y, --yes", "Skip confirmation prompt")
.action(pushAction);
}
4 changes: 4 additions & 0 deletions packages/cli/src/cli/program.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Command, Option } from "commander";
import { getAgentSkillsCommand } from "@/cli/commands/agent-skills/index.js";
import { getAgentsCommand } from "@/cli/commands/agents/index.js";
import { getAuthCommand } from "@/cli/commands/auth/index.js";
import { getLoginCommand } from "@/cli/commands/auth/login.js";
Expand Down Expand Up @@ -82,6 +83,9 @@ export function createProgram(context: CLIContext): Command {
// Register agents commands
program.addCommand(getAgentsCommand());

// Register agent-skills commands
program.addCommand(getAgentSkillsCommand());

// Register connectors commands
program.addCommand(getConnectorsCommand());

Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/core/project/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "@/core/project/schema.js";
import type { ProjectData, ProjectRoot } from "@/core/project/types.js";
import { agentResource } from "@/core/resources/agent/index.js";
import { agentSkillResource } from "@/core/resources/agent-skill/index.js";
import { authConfigResource } from "@/core/resources/auth-config/index.js";
import { connectorResource } from "@/core/resources/connector/index.js";
import type { Entity } from "@/core/resources/entity/index.js";
Expand Down Expand Up @@ -61,6 +62,7 @@ class ProjectConfigReader {
entities,
functions,
agents: localResources.agents,
agentSkills: localResources.agentSkills,
connectors: localResources.connectors,
authConfig: localResources.authConfig,
};
Expand Down Expand Up @@ -105,16 +107,17 @@ class ProjectConfigReader {
project: ProjectConfig,
): Promise<ProjectResources> {
const configDir = dirname(configPath);
const [entities, functions, agents, connectors, authConfig] =
const [entities, functions, agents, agentSkills, connectors, authConfig] =
await Promise.all([
entityResource.readAll(join(configDir, project.entitiesDir)),
functionResource.readAll(join(configDir, project.functionsDir)),
agentResource.readAll(join(configDir, project.agentsDir)),
agentSkillResource.readAll(join(configDir, project.agentSkillsDir)),
connectorResource.readAll(join(configDir, project.connectorsDir)),
authConfigResource.readAll(join(configDir, project.authDir)),
]);

return { entities, functions, agents, connectors, authConfig };
return { entities, functions, agents, agentSkills, connectors, authConfig };
}

private assertPluginProjectDoesNotLoadPlugins(
Expand Down Expand Up @@ -185,6 +188,7 @@ class ProjectConfigReader {
entities: markPluginEntities(resources.entities, namespace),
functions: namespacePluginFunctions(resources.functions, namespace),
agents: [],
agentSkills: [],
connectors: [],
authConfig: [],
};
Expand Down Expand Up @@ -241,6 +245,7 @@ class ProjectConfigReader {
entities,
functions,
agents: [],
agentSkills: [],
connectors: [],
authConfig: [],
};
Expand Down
26 changes: 22 additions & 4 deletions packages/cli/src/core/project/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { setAppVisibility } from "@/core/project/api.js";
import type { Visibility } from "@/core/project/schema.js";
import type { ProjectData } from "@/core/project/types.js";
import { agentResource } from "@/core/resources/agent/index.js";
import { agentSkillResource } from "@/core/resources/agent-skill/index.js";
import { authConfigResource } from "@/core/resources/auth-config/index.js";
import {
type ConnectorSyncResult,
Expand All @@ -23,12 +24,20 @@ import { deploySite } from "@/core/site/index.js";
* @returns true if there are entities, functions, agents, connectors, or a configured site to deploy
*/
export function hasResourcesToDeploy(projectData: ProjectData): boolean {
const { project, entities, functions, agents, connectors, authConfig } =
projectData;
const {
project,
entities,
functions,
agents,
agentSkills,
connectors,
authConfig,
} = projectData;
const hasSite = Boolean(project.site?.outputDirectory);
const hasEntities = entities.length > 0;
const hasFunctions = functions.length > 0;
const hasAgents = agents.length > 0;
const hasAgentSkills = agentSkills.length > 0;
const hasConnectors = connectors.length > 0;
const hasAuthConfig = authConfig.length > 0;
const hasVisibility = Boolean(project.visibility);
Expand All @@ -37,6 +46,7 @@ export function hasResourcesToDeploy(projectData: ProjectData): boolean {
hasEntities ||
hasFunctions ||
hasAgents ||
hasAgentSkills ||
hasConnectors ||
hasAuthConfig ||
hasVisibility ||
Expand Down Expand Up @@ -75,8 +85,15 @@ export async function deployAll(
projectData: ProjectData,
options?: DeployAllOptions,
): Promise<DeployAllResult> {
const { project, entities, functions, agents, connectors, authConfig } =
projectData;
const {
project,
entities,
functions,
agents,
agentSkills,
connectors,
authConfig,
} = projectData;

await setAppVisibility(project.visibility);
if (project.visibility) {
Expand All @@ -87,6 +104,7 @@ export async function deployAll(
onStart: options?.onFunctionStart,
onResult: options?.onFunctionResult,
});
await agentSkillResource.push(agentSkills);
await agentResource.push(agents);
await authConfigResource.push(authConfig);
// pushConnectors also reconciles: with an empty list it removes remote
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/core/project/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const ProjectConfigSchema = z.object({
entitiesDir: z.string().optional().default("entities"),
functionsDir: z.string().optional().default("functions"),
agentsDir: z.string().optional().default("agents"),
agentSkillsDir: z.string().optional().default("agent-skills"),
connectorsDir: z.string().optional().default("connectors"),
authDir: z.string().optional().default("auth"),
plugin: PluginMetadataSchema.optional(),
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/core/project/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ProjectConfig } from "@/core/project/schema.js";
import type { AgentConfig } from "@/core/resources/agent/index.js";
import type { AgentSkill } from "@/core/resources/agent-skill/index.js";
import type { AuthConfig } from "@/core/resources/auth-config/index.js";
import type { ConnectorResource } from "@/core/resources/connector/index.js";
import type { Entity } from "@/core/resources/entity/index.js";
Expand All @@ -20,6 +21,7 @@ export interface ProjectData {
entities: Entity[];
functions: BackendFunction[];
agents: AgentConfig[];
agentSkills: AgentSkill[];
connectors: ConnectorResource[];
authConfig: AuthConfig[];
}
72 changes: 72 additions & 0 deletions packages/cli/src/core/resources/agent-skill/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { KyResponse } from "ky";
import { getAppClient } from "@/core/clients/index.js";
import { ApiError, SchemaValidationError } from "@/core/errors.js";
import type {
AgentSkill,
ListAgentSkillsResponse,
SyncAgentSkillsResult,
} from "./schema.js";
import { ListAgentSkillsResponseSchema } from "./schema.js";

export async function fetchAgentSkills(): Promise<ListAgentSkillsResponse> {
const appClient = getAppClient();
let response: KyResponse;
try {
response = await appClient.get("agent-skills");
} catch (error) {
throw await ApiError.fromHttpError(error, "fetching agent skills");
}
const result = ListAgentSkillsResponseSchema.safeParse(await response.json());
if (!result.success) {
throw new SchemaValidationError(
"Invalid response from server",
result.error,
);
}
return result.data;
}

export async function pushAgentSkills(
skills: AgentSkill[],
): Promise<SyncAgentSkillsResult> {
if (skills.length === 0) {
return { created: [], updated: [], deleted: [] };
}

const appClient = getAppClient();
const remote = await fetchAgentSkills();
const remoteByName = new Map(remote.items.map((s) => [s.name, s]));
const localNames = new Set(skills.map((s) => s.name));

const created: string[] = [];
const updated: string[] = [];
const deleted: string[] = [];

try {
for (const skill of skills) {
const prev = remoteByName.get(skill.name);
if (!prev) {
await appClient.post("agent-skills", { json: skill });
created.push(skill.name);
} else if (
prev.description !== skill.description ||
prev.body !== skill.body
) {
await appClient.put(`agent-skills/${skill.name}`, {
json: { description: skill.description, body: skill.body },
});
updated.push(skill.name);
}
}
for (const remoteSkill of remote.items) {
if (!localNames.has(remoteSkill.name)) {
await appClient.delete(`agent-skills/${remoteSkill.name}`);
deleted.push(remoteSkill.name);
}
}
} catch (error) {
throw await ApiError.fromHttpError(error, "syncing agent skills");
Comment thread
yardend-wix marked this conversation as resolved.
}

return { created, updated, deleted };
}
Loading
Loading