A workspace command framework
Every workspace command.
One source of truth.
Define each operation once in TypeScript. Run it from the terminal, click it in a VS Code dashboard, or let an AI agent invoke it as a tool.
The problem
Every workspace collects a graveyard.
Each tool has its own conventions. None of them show up in your editor. None of them are discoverable by an AI assistant. And every new hire learns them by asking in Slack.
The idea
One file. Three surfaces.
Write a command once in TypeScript. Workmark renders it as a CLI, a VS Code form, and an AI-invocable tool — from the same definition, fully typed.
/** Build one or more packages. */import { cmd } from "@ldlework/workmark/define";import { buildable } from "../traits/buildable.js";export default cmd({needs: [buildable],handler: (_, { traits, sh }) =>sh(traits.buildable.command),});
$ wm build api web--- api ---compiled in 1.2s--- web ---compiled in 0.9s
> use_tool("build",{ project: ["api"] })→ compiled in 1.2s
Why it works
Three things, for the price of one.
Typed end-to-end
Traits carry zod schemas. Handlers destructure typed data from ctx. The CLI args, VS Code form fields, and MCP tool input schemas are all generated from the same declarations — no casts, no drift.
AI-native
Every command is an MCP tool. AI assistants discover and invoke your workspace the same way you do, with the same validated inputs. No server to run, no schema to maintain twice.
Zero SaaS
It's files in your repo. No accounts, no dashboards-as-a-service, no login. Works offline, commits to git, ships with your code.
For monorepos
One command, many projects.
Declare a trait once with a zod schema. Projects fulfill it with typed data. Commands ask for the trait and get per-project data, typed, in the handler.
import { z } from "zod";
import { defineTrait } from "@ldlework/workmark/define";
export const docker = defineTrait({
name: "docker",
schema: z.object({
composeFile: z.string(),
service: z.string(),
}),
});defineProject({
name: "api",
has: {
docker: {
composeFile: "docker-compose.yml",
service: "api",
},
},
});defineProject({
name: "web",
has: {
docker: {
composeFile: "docker-compose.yml",
service: "web",
},
},
});Now one command, and the project enum in CLI, VS Code, and the MCP tool schema is whichever projects fulfill docker.
export default cmd({
needs: [docker],
handler: (_, { traits, sh }) =>
sh(`docker compose -f ${traits.docker.composeFile} up -d ${traits.docker.service}`),
});wm up api
wm up api web # both in parallel
wm up --help # project: [api, web]Get started
Three steps.
- 1
Install
pnpm add -D @ldlework/workmark - 2
Write a command
.wm/commands/build.ts/** Build the project */ import { cmd } from "@ldlework/workmark/define"; export default cmd({ handler: (_, { sh }) => sh("cargo build"), }); - 3
Run it
wm build # CLI # or click it in the VS Code dashboard # or let Claude call it as an MCP tool