[{"data":1,"prerenderedAt":765},["ShallowReactive",2],{"navigation_docs":3,"-guide-cli":52,"-guide-cli-surround":760},[4,28],{"title":5,"path":6,"stem":7,"children":8,"icon":27},"Guide","\u002Fguide","1.guide\u002F01.index",[9,11,15,19,23],{"title":10,"path":6,"stem":7},"Getting Started",{"title":12,"path":13,"stem":14},"Defining Tools","\u002Fguide\u002Fdefining-tools","1.guide\u002F02.defining-tools",{"title":16,"path":17,"stem":18},"Validation and Errors","\u002Fguide\u002Fvalidation","1.guide\u002F03.validation",{"title":20,"path":21,"stem":22},"CLI","\u002Fguide\u002Fcli","1.guide\u002F04.cli",{"title":24,"path":25,"stem":26},"Moving a Package Over","\u002Fguide\u002Fmigrating","1.guide\u002F05.migrating","i-lucide-book-open",{"title":29,"path":30,"stem":31,"children":32,"icon":51},"Hosts","\u002Fhosts","2.hosts\u002F00.index",[33,35,39,43,47],{"title":34,"path":30,"stem":31},"Every host",{"title":36,"path":37,"stem":38},"MCP","\u002Fhosts\u002Fmcp","2.hosts\u002F01.mcp",{"title":40,"path":41,"stem":42},"Pi","\u002Fhosts\u002Fpi","2.hosts\u002F02.pi",{"title":44,"path":45,"stem":46},"OMP","\u002Fhosts\u002Fomp","2.hosts\u002F03.omp",{"title":48,"path":49,"stem":50},"AI SDK","\u002Fhosts\u002Fai-sdk","2.hosts\u002F04.ai-sdk","i-lucide-table",{"id":53,"title":54,"body":55,"description":751,"extension":752,"links":753,"meta":754,"navigation":756,"path":21,"seo":757,"stem":22,"__hash__":759},"docs\u002F1.guide\u002F04.cli.md","The CLI",{"type":56,"value":57,"toc":738},"minimark",[58,71,191,198,207,214,219,224,257,261,276,309,320,323,329,333,340,400,414,420,423,429,435,439,454,458,468,474,485,495,498,502,516,566,573,577,588,603,609,619,623,635,696,712,734],[59,60,61,62,66,67,70],"p",{},"Your tools already know their arguments. The schema says which ones are required, what type each one is and which values an enum takes. So why write a CLI by hand? ",[63,64,65],"code",{},"runCli"," reads all of that and gives every tool its own command. No CLI library underneath, just ",[63,68,69],{},"util.parseArgs"," from Node.",[72,73,79],"pre",{"className":74,"code":75,"filename":76,"language":77,"meta":78,"style":78},"language-ts shiki shiki-themes tools tools tools","import { runCli } from \"@agntn\u002Ftools\u002Fcli\";\nimport { slugTool } from \".\u002Ftools.ts\";\n\nawait runCli({\n  name: \"slugs\",\n  version: \"1.0.0\",\n  description: \"Slugs from titles\",\n  tools: [slugTool],\n  mcp: true,\n});\n","src\u002Fcli.ts","ts","",[63,80,81,104,119,126,139,151,162,173,179,185],{"__ignoreMap":78},[82,83,86,90,94,97,101],"span",{"class":84,"line":85},"line",1,[82,87,89],{"class":88},"skH_V","import",[82,91,93],{"class":92},"s38Sx"," { runCli } ",[82,95,96],{"class":88},"from",[82,98,100],{"class":99},"shU9J"," \"@agntn\u002Ftools\u002Fcli\"",[82,102,103],{"class":92},";\n",[82,105,107,109,112,114,117],{"class":84,"line":106},2,[82,108,89],{"class":88},[82,110,111],{"class":92}," { slugTool } ",[82,113,96],{"class":88},[82,115,116],{"class":99}," \".\u002Ftools.ts\"",[82,118,103],{"class":92},[82,120,122],{"class":84,"line":121},3,[82,123,125],{"emptyLinePlaceholder":124},true,"\n",[82,127,129,132,136],{"class":84,"line":128},4,[82,130,131],{"class":88},"await",[82,133,135],{"class":134},"sK71F"," runCli",[82,137,138],{"class":92},"({\n",[82,140,142,145,148],{"class":84,"line":141},5,[82,143,144],{"class":92},"  name: ",[82,146,147],{"class":99},"\"slugs\"",[82,149,150],{"class":92},",\n",[82,152,154,157,160],{"class":84,"line":153},6,[82,155,156],{"class":92},"  version: ",[82,158,159],{"class":99},"\"1.0.0\"",[82,161,150],{"class":92},[82,163,165,168,171],{"class":84,"line":164},7,[82,166,167],{"class":92},"  description: ",[82,169,170],{"class":99},"\"Slugs from titles\"",[82,172,150],{"class":92},[82,174,176],{"class":84,"line":175},8,[82,177,178],{"class":92},"  tools: [slugTool],\n",[82,180,182],{"class":84,"line":181},9,[82,183,184],{"class":92},"  mcp: true,\n",[82,186,188],{"class":84,"line":187},10,[82,189,190],{"class":92},"});\n",[59,192,193,194,197],{},"Point your ",[63,195,196],{},"bin"," at the built file and that's it:",[72,199,205],{"className":200,"code":202,"filename":203,"language":204,"meta":78},[201],"language-text","$ slugs slug --text \"Crème brûlée\"\ncreme-brulee\n","terminal","text",[63,206,202],{"__ignoreMap":78},[59,208,209,210,213],{},"It's the one Node part of the package. The core and the host adapters run in a browser tab, this one reads ",[63,211,212],{},"process.argv",".",[215,216,218],"h2",{"id":217},"how-a-tool-turns-into-a-command","How a tool turns into a command",[220,221,223],"h3",{"id":222},"where-does-the-command-name-come-from","Where does the command name come from?",[59,225,226,227,230,231,234,235,230,238,241,242,245,246,249,250,253,254,213],{},"From the tool name, minus the package prefix. ",[63,228,229],{},"text_slug"," becomes ",[63,232,233],{},"slug",", ",[63,236,237],{},"hashes_digest_identify",[63,239,240],{},"digest-identify",". Want another word? The ",[63,243,244],{},"cli"," hint on ",[63,247,248],{},"defineTool"," takes a ",[63,251,252],{},"command"," and a few ",[63,255,256],{},"aliases",[220,258,260],{"id":259},"flags-straight-from-the-schema","Flags straight from the schema",[59,262,263,264,267,268,271,272,275],{},"Every property is a flag, in kebab case. ",[63,265,266],{},"maxWords"," and ",[63,269,270],{},"max_words"," both turn into ",[63,273,274],{},"--max-words",". Strings go in as they are. Integers and booleans get converted against the schema. Objects and arrays come in as JSON:",[72,277,281],{"className":278,"code":279,"filename":203,"language":280,"meta":78,"style":78},"language-bash shiki shiki-themes tools tools tools","$ slugs batch --titles '[\"Hello World\", \"Crème brûlée\"]' --max-words 2 --no-lower\n","bash",[63,282,283],{"__ignoreMap":78},[82,284,285,288,291,294,297,300,303,306],{"class":84,"line":85},[82,286,287],{"class":134},"$",[82,289,290],{"class":99}," slugs",[82,292,293],{"class":99}," batch",[82,295,296],{"class":99}," --titles",[82,298,299],{"class":99}," '[\"Hello World\", \"Crème brûlée\"]'",[82,301,302],{"class":99}," --max-words",[82,304,305],{"class":99}," 2",[82,307,308],{"class":99}," --no-lower\n",[59,310,311,312,315,316,319],{},"A boolean answers to ",[63,313,314],{},"--lower"," and to ",[63,317,318],{},"--no-lower",". Say both and the command refuses, it doesn't pick a winner. Same for an unknown option, a missing value or a word too many. Nothing on the line is quietly skipped.",[59,321,322],{},"Then the core validates, like on every other host. A typo here gets the same treatment a model's typo gets:",[72,324,327],{"className":325,"code":326,"filename":203,"language":204,"meta":78},[201],"$ slugs slug --text Hello --sep _\nInvalid arguments: unknown option \"--sep\"; takes --text, --separator, --json\n$ slugs slug --text Hello --separator \"~\"\nInvalid arguments at \u002Fseparator: must be one of -, _, .\n",[63,328,326],{"__ignoreMap":78},[220,330,332],{"id":331},"positionals-and-stdin","Positionals and stdin",[59,334,335,336,339],{},"Typing ",[63,337,338],{},"--text"," every time gets old fast. Two more hints fix that:",[72,341,344],{"className":74,"code":342,"filename":343,"language":77,"meta":78,"style":78},"export const slugTool = defineTool({\n  name: \"text_slug\",\n  \u002F\u002F …\n  cli: { positional: [\"text\"], stdin: [\"text\"] },\n});\n","tools.ts",[63,345,346,365,374,380,396],{"__ignoreMap":78},[82,347,348,351,354,357,360,363],{"class":84,"line":85},[82,349,350],{"class":88},"export",[82,352,353],{"class":88}," const",[82,355,356],{"class":92}," slugTool ",[82,358,359],{"class":88},"=",[82,361,362],{"class":134}," defineTool",[82,364,138],{"class":92},[82,366,367,369,372],{"class":84,"line":106},[82,368,144],{"class":92},[82,370,371],{"class":99},"\"text_slug\"",[82,373,150],{"class":92},[82,375,376],{"class":84,"line":121},[82,377,379],{"class":378},"scIB-","  \u002F\u002F …\n",[82,381,382,385,388,391,393],{"class":84,"line":128},[82,383,384],{"class":92},"  cli: { positional: [",[82,386,387],{"class":99},"\"text\"",[82,389,390],{"class":92},"], stdin: [",[82,392,387],{"class":99},[82,394,395],{"class":92},"] },\n",[82,397,398],{"class":84,"line":141},[82,399,190],{"class":92},[59,401,402,405,406,409,410,413],{},[63,403,404],{},"positional"," takes properties in order, as plain words. ",[63,407,408],{},"stdin"," lets a string property read standard input when you pass ",[63,411,412],{},"-",":",[72,415,418],{"className":416,"code":417,"filename":203,"language":204,"meta":78},[201],"$ slugs slug \"Hello, World\" --separator _\nhello_world\n$ echo \"Pipe me in\" | slugs slug -\npipe-me-in\n",[63,419,417],{"__ignoreMap":78},[59,421,422],{},"Stdin has to be valid UTF-8. A lenient decode would swap bad bytes for U+FFFD, and a hash tool would happily hash something you never sent. So invalid input is an error. Got real binary data? That's a job for a command of your own, see below.",[59,424,425,428],{},[63,426,427],{},"--help"," is built from the same fields. Descriptions come from the schema, the enum values too:",[72,430,433],{"className":431,"code":432,"language":204,"meta":78},[201],"Turn a title into a lowercase URL slug. (slugs slug v1.0.0)\n\nUSAGE slugs slug [OPTIONS] \u003CTEXT>\n\nARGUMENTS\n\n  TEXT  Title to slug (- reads stdin) (required)\n\nOPTIONS\n\n  --separator=\u003C-|_|.>  Joins the words. '-' when omitted\n  --json               Print the details as JSON instead of the text\n  -h, --help           Show this help\n",[63,434,432],{"__ignoreMap":78},[220,436,438],{"id":437},"when-does-a-broken-hint-show-up","When does a broken hint show up?",[59,440,441,442,445,446,449,450,453],{},"At startup, before any command runs. A hint naming a property the tool doesn't have, two properties fighting over one flag, a boolean called ",[63,443,444],{},"noCache",", a required positional after an optional one. Each one fails in ",[63,447,448],{},"createCli"," with a ",[63,451,452],{},"ToolDefinitionError",". Not on the first user who types the wrong thing.",[215,455,457],{"id":456},"what-comes-out","What comes out",[59,459,460,461,464,465,413],{},"The command prints the tool's text, the same text a model reads. Want data instead? ",[63,462,463],{},"--json"," prints ",[63,466,467],{},"details",[72,469,472],{"className":470,"code":471,"filename":203,"language":204,"meta":78},[201],"$ slugs slug \"Hello\" --json\n{\n  \"slug\": \"hello\",\n  \"words\": 1\n}\n",[63,473,471],{"__ignoreMap":78},[59,475,476,477,480,481,484],{},"A returned ",[63,478,479],{},"isError"," goes to stderr with exit code 1. So ",[63,482,483],{},"slugs slug \"!!!\""," fails like a proper command, scripts included.",[59,486,487,488,490,491,494],{},"A thrown error is a different story. Tell ",[63,489,65],{}," which errors you throw on purpose through ",[63,492,493],{},"expected",", and those print as one line with exit code 1. Anything else keeps its stack trace. A bug should look like a bug.",[59,496,497],{},"Errors get the same treatment as on the other hosts: one line each, so a newline in an echoed argument can't forge a second one. Escape codes and bidi tricks don't reach your terminal, not even through the tool's own text. And there are no colors at all, so nothing breaks in a pipe.",[215,499,501],{"id":500},"commands-only-the-cli-gets","Commands only the CLI gets",[59,503,504,505,508,509,512,513,413],{},"Some things belong on the command line and nowhere else. A ",[63,506,507],{},"version"," with build details, a ",[63,510,511],{},"doctor",", a reader for binary stdin. Write them as tool definitions and pass them as ",[63,514,515],{},"commands",[72,517,519],{"className":74,"code":518,"filename":76,"language":77,"meta":78,"style":78},"await runCli({\n  name: \"slugs\",\n  version: \"1.0.0\",\n  description: \"Slugs from titles\",\n  tools: [slugTool],\n  commands: [separatorsCommand],\n});\n",[63,520,521,529,537,545,553,557,562],{"__ignoreMap":78},[82,522,523,525,527],{"class":84,"line":85},[82,524,131],{"class":88},[82,526,135],{"class":134},[82,528,138],{"class":92},[82,530,531,533,535],{"class":84,"line":106},[82,532,144],{"class":92},[82,534,147],{"class":99},[82,536,150],{"class":92},[82,538,539,541,543],{"class":84,"line":121},[82,540,156],{"class":92},[82,542,159],{"class":99},[82,544,150],{"class":92},[82,546,547,549,551],{"class":84,"line":128},[82,548,167],{"class":92},[82,550,170],{"class":99},[82,552,150],{"class":92},[82,554,555],{"class":84,"line":141},[82,556,178],{"class":92},[82,558,559],{"class":84,"line":153},[82,560,561],{"class":92},"  commands: [separatorsCommand],\n",[82,563,564],{"class":84,"line":164},[82,565,190],{"class":92},[59,567,568,569,572],{},"MCP, Pi, OMP and the AI SDK never see them. They parse, validate and print exactly like the tools. One with the name of a generated command takes its place, ",[63,570,571],{},"mcp"," included.",[220,574,576],{"id":575},"can-it-guess-what-i-meant","Can it guess what I meant?",[59,578,579,580,583,584,587],{},"If you let it. ",[63,581,582],{},"default"," names the command for an empty line. ",[63,585,586],{},"fallback"," takes any first word that isn't a command:",[72,589,591],{"className":74,"code":590,"language":77,"meta":78,"style":78},"fallback: \"slug\",\n",[63,592,593],{"__ignoreMap":78},[82,594,595,598,601],{"class":84,"line":85},[82,596,597],{"class":92},"fallback: ",[82,599,600],{"class":99},"\"slug\"",[82,602,150],{"class":92},[72,604,607],{"className":605,"code":606,"filename":203,"language":204,"meta":78},[201],"$ slugs \"Hello, World\"\nhello-world\n",[63,608,606],{"__ignoreMap":78},[59,610,611,612,234,615,618],{},"Just remember it's greedy. Without ",[63,613,614],{},"mcp: true",[63,616,617],{},"slugs mcp"," slugs the word \"mcp\". Correct, and completely useless.",[215,620,622],{"id":621},"an-mcp-server-on-the-side","An MCP server on the side",[59,624,625,627,628,630,631,634],{},[63,626,614],{}," adds an ",[63,629,571],{}," command. It's ",[63,632,633],{},"createMcpServer"," over stdio, with the CLI's name and version as the server info:",[72,636,641],{"className":637,"code":638,"filename":639,"language":640,"meta":78,"style":78},"language-json shiki shiki-themes tools tools tools","{\n  \"mcpServers\": {\n    \"slugs\": { \"command\": \"slugs\", \"args\": [\"mcp\"] }\n  }\n}\n",".mcp.json","json",[63,642,643,648,656,686,691],{"__ignoreMap":78},[82,644,645],{"class":84,"line":85},[82,646,647],{"class":92},"{\n",[82,649,650,653],{"class":84,"line":106},[82,651,652],{"class":134},"  \"mcpServers\"",[82,654,655],{"class":92},": {\n",[82,657,658,661,664,667,670,672,674,677,680,683],{"class":84,"line":121},[82,659,660],{"class":134},"    \"slugs\"",[82,662,663],{"class":92},": { ",[82,665,666],{"class":134},"\"command\"",[82,668,669],{"class":92},": ",[82,671,147],{"class":99},[82,673,234],{"class":92},[82,675,676],{"class":134},"\"args\"",[82,678,679],{"class":92},": [",[82,681,682],{"class":99},"\"mcp\"",[82,684,685],{"class":92},"] }\n",[82,687,688],{"class":84,"line":128},[82,689,690],{"class":92},"  }\n",[82,692,693],{"class":84,"line":141},[82,694,695],{"class":92},"}\n",[59,697,698,699,702,703,705,706,709,710,213],{},"One binary, two jobs. The server serves ",[63,700,701],{},"tools"," only, your own ",[63,704,515],{}," stay on the command line. It needs ",[63,707,708],{},"@modelcontextprotocol\u002Fserver"," installed, and loads it only when someone actually runs ",[63,711,571],{},[59,713,714,715,718,719,722,723,726,727,729,730,733],{},"Progress is the one thing that differs. A plain command has nowhere to show it, so ",[63,716,717],{},"progress"," is ",[63,720,721],{},"undefined"," there and your ",[63,724,725],{},"progress?.()"," calls stay quiet. Through ",[63,728,617],{}," it's a real MCP server, so a client that sends a ",[63,731,732],{},"progressToken"," gets every line.",[735,736,737],"style",{},"html pre.shiki code .skH_V, html code.shiki .skH_V{--shiki-light:var(--shiki-token-keyword);--shiki-default:var(--shiki-token-keyword);--shiki-dark:var(--shiki-token-keyword)}html pre.shiki code .s38Sx, html code.shiki .s38Sx{--shiki-light:var(--ui-text-highlighted);--shiki-default:var(--ui-text-highlighted);--shiki-dark:var(--ui-text-highlighted)}html pre.shiki code .shU9J, html code.shiki .shU9J{--shiki-light:var(--shiki-token-string);--shiki-default:var(--shiki-token-string);--shiki-dark:var(--shiki-token-string)}html pre.shiki code .sK71F, html code.shiki .sK71F{--shiki-light:var(--shiki-token-function);--shiki-default:var(--shiki-token-function);--shiki-dark:var(--shiki-token-function)}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .scIB-, html code.shiki .scIB-{--shiki-light:var(--shiki-token-comment);--shiki-default:var(--shiki-token-comment);--shiki-dark:var(--shiki-token-comment)}",{"title":78,"searchDepth":106,"depth":106,"links":739},[740,746,747,750],{"id":217,"depth":106,"text":218,"children":741},[742,743,744,745],{"id":222,"depth":121,"text":223},{"id":259,"depth":121,"text":260},{"id":331,"depth":121,"text":332},{"id":437,"depth":121,"text":438},{"id":456,"depth":106,"text":457},{"id":500,"depth":106,"text":501,"children":748},[749],{"id":575,"depth":121,"text":576},{"id":621,"depth":106,"text":622},"Turn the same tool list into a command line. Flags come from the schema and MCP rides along","md",null,{"icon":755},"i-lucide-square-terminal",{"title":20},{"title":758,"description":751},"A command line from the tools you already have","i6m6O6wAWGvNCQVpK2vH85R_qg0P5XnqenbPJuM9bas",[761,763],{"title":16,"path":17,"stem":18,"description":762,"children":-1},"How the core checks every call and how each host reports a failure. And why error text never gets a second line",{"title":24,"path":25,"stem":26,"description":764,"children":-1},"Take a package that declares its tools four times and bring it down to one definition. Step by step with the traps marked",1791296560327]