clfly(active)
File-based routing for CLIs — one commands/ tree becomes CLI, completions, and MCP.
npm create clfly mycliFile-based routing for CLIs — one commands/ tree becomes CLI, completions, and MCP.
npm create clfly mycliOne commands/ tree of schema-declared functions → argv (--help), shell
completions, and mcp serve. Schema once, derive the interface (PostgREST’s
move on CLIs).
Migration: wrap an existing CLI as schemas over shell-outs and get an agent-callable surface without rewriting it.
Router, parsing, validation, help/version, build manifest, bash/zsh/fish
completions, --json, and mcp serve (stdio, --root scoping, optional output
schemas / structured content, session store for large results). Self-hosting:
clfly scaffolds clfly CLIs via its own tree. Spec is implementation-neutral.
Contract is Standard JSON Schema. clfly asks every schema for
~standard.jsonSchema.input({ target: "draft-07" }) and refuses vendors that
can’t produce it:
export function toJsonSchema(schema: AnySchema): JsonSchema {
const std = schema["~standard"];
if (std.jsonSchema?.input) {
return std.jsonSchema.input({ target: "draft-07" }) as JsonSchema;
}
throw new ClflyError(
`Schema (vendor: ${std.vendor}) does not implement Standard JSON Schema ` +
`(~standard.jsonSchema). Use Zod 4, ArkType, or Valibot with JSON Schema support.`,
);
}
Vendor-specific reads stay optional sugar (Zod .meta({ alias }) for short
flags). One draft-07 document feeds three readers.
Argv. Position in the tree decides surface: [param] segments become
positionals (also answer to --name); an exported positionals tuple claims
the rest; remaining properties become flags. Tokenizer coerces booleans only
(for --no-*); numbers stay strings until the validator owns types —
z.coerce.number() converts and applies defaults. Avoids two type systems
disagreeing at the tokenizer/validator boundary. --help renders from the same
records.
MCP. Path → tool name by dropping dynamics and joining with underscores
(users/[id]/show → users_show). Path params merge back as required string
properties — argv carries data in the address; MCP is flat. Collisions hard-fail.
Optional output schemas → outputSchema / structured content. Large results
spool under .clfly/sessions/.
Completions. Bash/zsh/fish from the same build-manifest records.
commands/users/[id]/show.ts:
import { z } from "zod";
import type { Context } from "@clfly/core";
export const meta = { description: "Show a single user" };
export const args = z.object({
id: z.string().describe("User id"),
});
export default async function (opts: z.infer<typeof args>, ctx: Context) {
const user = { id: opts.id, status: "active" };
if (ctx.json) return user;
ctx.stdout.write(`${user.id}\t${user.status}\n`);
}
Human help:
Usage: demo users <id> show [options]
Show a single user
Arguments:
<id> User id (also --id)
Options:
-h, --help Show help
-V, --version Show version
--json Emit JSON
Agent tool:
{
"name": "users_show",
"description": "Show a single user",
"inputSchema": {
"type": "object",
"properties": { "id": { "type": "string", "description": "User id" } },
"required": ["id"]
}
}
One file, one schema. .describe("User id") appears in both because both are
projections.
Dev: live filesystem walk ([name] → dynamic segments, index → directory
leaves). Prod: build bakes the walk into a manifest — flags, positionals,
schemas precomputed, handlers behind lazy () => import(...) thunks.
createCli prefers the manifest. Zero-build dev loop; shipped CLI pays no scan
cost at startup. Manifest doubles as completions input.