[{"data":1,"prerenderedAt":1496},["ShallowReactive",2],{"navigation_docs":3,"-guide-cli":52,"-guide-cli-surround":1491},[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":1482,"extension":1483,"links":1484,"meta":1485,"navigation":1487,"path":21,"seo":1488,"stem":22,"__hash__":1490},"docs\u002F1.guide\u002F04.cli.md","The CLI",{"type":56,"value":57,"toc":1465},"minimark",[58,71,191,198,207,214,219,224,257,261,288,321,332,343,349,355,359,366,426,440,446,449,455,461,476,480,486,512,522,528,552,562,612,630,634,647,658,664,667,673,695,717,720,739,745,757,761,783,787,797,803,814,824,827,831,845,895,902,906,909,961,974,978,984,1002,1155,1183,1187,1198,1213,1219,1229,1247,1251,1264,1324,1333,1401,1427,1443,1461],[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,276,279,280,283,284,287],{},"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. Numbers have to look like numbers: ",[63,277,278],{},"--max-words 2"," works, ",[63,281,282],{},"--max-words 1.5"," gets refused instead of rounded down. And ",[63,285,286],{},"0x10","? Just a typo here. Objects and arrays come in as JSON:",[72,289,293],{"className":290,"code":291,"filename":203,"language":292,"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,294,295],{"__ignoreMap":78},[82,296,297,300,303,306,309,312,315,318],{"class":84,"line":85},[82,298,299],{"class":134},"$",[82,301,302],{"class":99}," slugs",[82,304,305],{"class":99}," batch",[82,307,308],{"class":99}," --titles",[82,310,311],{"class":99}," '[\"Hello World\", \"Crème brûlée\"]'",[82,313,314],{"class":99}," --max-words",[82,316,317],{"class":99}," 2",[82,319,320],{"class":99}," --no-lower\n",[59,322,323,324,327,328,331],{},"A boolean answers to ",[63,325,326],{},"--lower"," and to ",[63,329,330],{},"--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,333,334,335,338,339,342],{},"Then the core validates, like on every other host. Same checks a model's typo gets, but the answer speaks command line. You typed ",[63,336,337],{},"--separator",", not ",[63,340,341],{},"\u002Fseparator",", so that's the name you get back:",[72,344,347],{"className":345,"code":346,"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 --separator: must be one of -, _, .\n$ slugs slug\nInvalid arguments: missing --text\n",[63,348,346],{"__ignoreMap":78},[59,350,351,352,354],{},"Every other host keeps the JSON pointer. A model reads ",[63,353,341],{}," just fine.",[220,356,358],{"id":357},"positionals-and-stdin","Positionals and stdin",[59,360,361,362,365],{},"Typing ",[63,363,364],{},"--text"," every time gets old fast. Two more hints fix that:",[72,367,370],{"className":74,"code":368,"filename":369,"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,371,372,391,400,406,422],{"__ignoreMap":78},[82,373,374,377,380,383,386,389],{"class":84,"line":85},[82,375,376],{"class":88},"export",[82,378,379],{"class":88}," const",[82,381,382],{"class":92}," slugTool ",[82,384,385],{"class":88},"=",[82,387,388],{"class":134}," defineTool",[82,390,138],{"class":92},[82,392,393,395,398],{"class":84,"line":106},[82,394,144],{"class":92},[82,396,397],{"class":99},"\"text_slug\"",[82,399,150],{"class":92},[82,401,402],{"class":84,"line":121},[82,403,405],{"class":404},"scIB-","  \u002F\u002F …\n",[82,407,408,411,414,417,419],{"class":84,"line":128},[82,409,410],{"class":92},"  cli: { positional: [",[82,412,413],{"class":99},"\"text\"",[82,415,416],{"class":92},"], stdin: [",[82,418,413],{"class":99},[82,420,421],{"class":92},"] },\n",[82,423,424],{"class":84,"line":141},[82,425,190],{"class":92},[59,427,428,431,432,435,436,439],{},[63,429,430],{},"positional"," takes properties in order, as plain words. ",[63,433,434],{},"stdin"," lets a string property read standard input when you pass ",[63,437,438],{},"-",":",[72,441,444],{"className":442,"code":443,"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,445,443],{"__ignoreMap":78},[59,447,448],{},"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,450,451,454],{},[63,452,453],{},"--help"," is built from the same fields. Descriptions come from the schema, the enum values too:",[72,456,459],{"className":457,"code":458,"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,460,458],{"__ignoreMap":78},[59,462,463,464,467,468,471,472,475],{},"Errors borrow these names. Forget the title and you get ",[63,465,466],{},"missing \u003CTEXT>",", a blank one says ",[63,469,470],{},"at \u003CTEXT>: must match pattern \"\\S\"",". Where's ",[63,473,474],{},"\u002Ftext","? Back with the other hosts.",[220,477,479],{"id":478},"short-flags-and-the-rest-of-the-line","Short flags and the rest of the line",[59,481,482,483,485],{},"Quotes around every title get old too. So does ",[63,484,337],{},". Two hints bring back what every CLI written by hand had:",[72,487,489],{"className":74,"code":488,"filename":369,"language":77,"meta":78,"style":78},"cli: { rest: \"text\", stdin: [\"text\"], short: { separator: \"s\" } },\n",[63,490,491],{"__ignoreMap":78},[82,492,493,496,498,501,503,506,509],{"class":84,"line":85},[82,494,495],{"class":92},"cli: { rest: ",[82,497,413],{"class":99},[82,499,500],{"class":92},", stdin: [",[82,502,413],{"class":99},[82,504,505],{"class":92},"], short: { separator: ",[82,507,508],{"class":99},"\"s\"",[82,510,511],{"class":92}," } },\n",[59,513,514,517,518,521],{},[63,515,516],{},"rest"," takes every word the positionals leave over and joins them with single spaces. ",[63,519,520],{},"short"," gives an option an alias of one letter:",[72,523,526],{"className":524,"code":525,"filename":203,"language":204,"meta":78},[201],"$ slugs slug Hello big world -s _\nhello_big_world\n",[63,527,525],{"__ignoreMap":78},[59,529,530,531,279,534,537,538,540,541,544,545,548,549,551],{},"Only a whole word counts as a short flag, though. ",[63,532,533],{},"-s _",[63,535,536],{},"-s_"," is an unknown option. Pedantic? A bit. But ",[63,539,69],{}," reads ",[63,542,543],{},"-separator _"," as ",[63,546,547],{},"-s eparator",", and next to a greedy ",[63,550,516],{}," one dash too few shouldn't turn into some other call. Here it fails and tells you why.",[59,553,554,555,558,559,561],{},"Joining is right for a title and wrong for a list. Glue ",[63,556,557],{},"\"jacque fresco\" venus"," together with spaces and nobody can tell anymore which words came as a pair. Make the property an array of strings and ",[63,560,516],{}," keeps each word as typed, one item apiece:",[72,563,565],{"className":74,"code":564,"filename":369,"language":77,"meta":78,"style":78},"input: Type.Object({ digest: Type.String(), words: Type.Array(Type.String()) }),\ncli: { positional: [\"digest\"], rest: \"words\" },\n",[63,566,567,595],{"__ignoreMap":78},[82,568,569,572,575,578,581,584,587,590,592],{"class":84,"line":85},[82,570,571],{"class":92},"input: Type.",[82,573,574],{"class":134},"Object",[82,576,577],{"class":92},"({ digest: Type.",[82,579,580],{"class":134},"String",[82,582,583],{"class":92},"(), words: Type.",[82,585,586],{"class":134},"Array",[82,588,589],{"class":92},"(Type.",[82,591,580],{"class":134},[82,593,594],{"class":92},"()) }),\n",[82,596,597,600,603,606,609],{"class":84,"line":106},[82,598,599],{"class":92},"cli: { positional: [",[82,601,602],{"class":99},"\"digest\"",[82,604,605],{"class":92},"], rest: ",[82,607,608],{"class":99},"\"words\"",[82,610,611],{"class":92}," },\n",[59,613,614,617,618,621,622,625,626,629],{},[63,615,616],{},"hashes search 5d41 \"jacque fresco\" venus"," hands ",[63,619,620],{},"execute"," the words ",[63,623,624],{},"[\"jacque fresco\", \"venus\"]",". Those quotes you typed finally count for something. Each item reads like any other positional word. Then the core checks the array as on every surface, ",[63,627,628],{},"minItems"," and enum items included.",[220,631,633],{"id":632},"text-that-starts-with-a-dash","Text that starts with a dash",[59,635,636,637,640,641,643,644,213],{},"Is ",[63,638,639],{},"-.-."," an option? ",[63,642,69],{}," thinks so. In Morse it's a C. Every PGP armor line opens with five dashes, and a temperature can be ",[63,645,646],{},"-12",[59,648,649,650,234,652,234,654,657],{},"So a word counts as an option only when it spells one as a whole: ",[63,651,337],{},[63,653,330],{},[63,655,656],{},"-s",". Any other dashed word takes the next free positional:",[72,659,662],{"className":660,"code":661,"filename":203,"language":204,"meta":78},[201],"$ slugs slug \"-12 below zero\"\n12-below-zero\n",[63,663,661],{"__ignoreMap":78},[59,665,666],{},"Free means the plain words left room for it. Once they fill every positional, a dashed word is an unknown option again. A typo after the title fails, it doesn't end up in your slug:",[72,668,671],{"className":669,"code":670,"filename":203,"language":204,"meta":78},[201],"$ slugs slug Hello --seperator _\nInvalid arguments: unknown option \"--seperator\"; takes --separator (-s), --json\n",[63,672,670],{"__ignoreMap":78},[59,674,675,677,678,681,682,267,685,688,689,691,692,213],{},[63,676,516],{}," counts as one positional here. ",[63,679,680],{},"slugs slug -12 below zero"," fails, because ",[63,683,684],{},"below",[63,686,687],{},"zero"," already took the spot. Quote the title and it's one word again. Text that does spell a flag, like ",[63,690,656],{}," itself, goes after ",[63,693,694],{},"--",[59,696,697,698,701,702,705,706,709,710,713,714,213],{},"Great for Morse, less great for an id. No collection is called ",[63,699,700],{},"--withPubkey",", but ",[63,703,704],{},"list --withPubkey"," hands it to ",[63,707,708],{},"collection"," anyway and the command answers ",[63,711,712],{},"Unknown collection",". Wrong word blamed. The flag you wanted was ",[63,715,716],{},"--with-pubkey",[59,718,719],{},"So tell the CLI which positionals take plain words only:",[72,721,723],{"className":74,"code":722,"filename":369,"language":77,"meta":78,"style":78},"cli: { positional: [\"collection\"], plain: [\"collection\"] },\n",[63,724,725],{"__ignoreMap":78},[82,726,727,729,732,735,737],{"class":84,"line":85},[82,728,599],{"class":92},[82,730,731],{"class":99},"\"collection\"",[82,733,734],{"class":92},"], plain: [",[82,736,731],{"class":99},[82,738,421],{"class":92},[72,740,743],{"className":741,"code":742,"filename":203,"language":204,"meta":78},[201],"$ puzzles list --withPubkey\nInvalid arguments: unknown option \"--withPubkey\"; takes --with-pubkey, --json\n",[63,744,742],{"__ignoreMap":78},[59,746,747,748,750,751,753,754,756],{},"That's the typo named, with the flags the command does take, and not one check in ",[63,749,620],{},". A ",[63,752,516],{}," can be plain too, then each word it takes has to be. Got an id that really starts with a dash? ",[63,755,694],{}," still hands it over.",[220,758,760],{"id":759},"when-does-a-broken-hint-show-up","When does a broken hint show up?",[59,762,763,764,767,768,234,771,774,775,778,779,782],{},"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,765,766],{},"noCache",", a required positional after an optional one, a short flag that tries to take ",[63,769,770],{},"-h",[63,772,773],{},"plain"," on a flag. Each one fails in ",[63,776,777],{},"createCli"," with a ",[63,780,781],{},"ToolDefinitionError",". Not on the first user who types the wrong thing.",[215,784,786],{"id":785},"what-comes-out","What comes out",[59,788,789,790,793,794,439],{},"The command prints the tool's text, the same text a model reads. Want data instead? ",[63,791,792],{},"--json"," prints ",[63,795,796],{},"details",[72,798,801],{"className":799,"code":800,"filename":203,"language":204,"meta":78},[201],"$ slugs slug \"Hello\" --json\n{\n  \"slug\": \"hello\",\n  \"words\": 1\n}\n",[63,802,800],{"__ignoreMap":78},[59,804,805,806,809,810,813],{},"A returned ",[63,807,808],{},"isError"," goes to stderr with exit code 1. So ",[63,811,812],{},"slugs slug \"!!!\""," fails like a proper command, scripts included.",[59,815,816,817,819,820,823],{},"A thrown error is a different story. Tell ",[63,818,65],{}," which errors you throw on purpose through ",[63,821,822],{},"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,825,826],{},"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,828,830],{"id":829},"commands-only-the-cli-gets","Commands only the CLI gets",[59,832,833,834,837,838,841,842,439],{},"Some things belong on the command line and nowhere else. A ",[63,835,836],{},"version"," with build details, a ",[63,839,840],{},"doctor",", a reader for binary stdin. Write them as tool definitions and pass them as ",[63,843,844],{},"commands",[72,846,848],{"className":74,"code":847,"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,849,850,858,866,874,882,886,891],{"__ignoreMap":78},[82,851,852,854,856],{"class":84,"line":85},[82,853,131],{"class":88},[82,855,135],{"class":134},[82,857,138],{"class":92},[82,859,860,862,864],{"class":84,"line":106},[82,861,144],{"class":92},[82,863,147],{"class":99},[82,865,150],{"class":92},[82,867,868,870,872],{"class":84,"line":121},[82,869,156],{"class":92},[82,871,159],{"class":99},[82,873,150],{"class":92},[82,875,876,878,880],{"class":84,"line":128},[82,877,167],{"class":92},[82,879,170],{"class":99},[82,881,150],{"class":92},[82,883,884],{"class":84,"line":141},[82,885,178],{"class":92},[82,887,888],{"class":84,"line":153},[82,889,890],{"class":92},"  commands: [separatorsCommand],\n",[82,892,893],{"class":84,"line":164},[82,894,190],{"class":92},[59,896,897,898,901],{},"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,899,900],{},"mcp"," included.",[220,903,905],{"id":904},"what-about-bytes-going-out","What about bytes going out?",[59,907,908],{},"The CLI ends every text with a newline. Lovely for a slug. Quietly fatal for a gzip file, which comes out one byte too long. Nobody notices until a checksum does. So a command that writes bytes writes them itself and answers with no content blocks at all:",[72,910,912],{"className":74,"code":911,"language":77,"meta":78,"style":78},"cli: { json: false },\nexecute({ file }) {\n  process.stdout.write(gunzipSync(readFileSync(file)));\n  return { content: [], details: null };\n},\n",[63,913,914,919,926,948,956],{"__ignoreMap":78},[82,915,916],{"class":84,"line":85},[82,917,918],{"class":92},"cli: { json: false },\n",[82,920,921,923],{"class":84,"line":106},[82,922,620],{"class":134},[82,924,925],{"class":92},"({ file }) {\n",[82,927,928,931,934,937,940,942,945],{"class":84,"line":121},[82,929,930],{"class":92},"  process.stdout.",[82,932,933],{"class":134},"write",[82,935,936],{"class":92},"(",[82,938,939],{"class":134},"gunzipSync",[82,941,936],{"class":92},[82,943,944],{"class":134},"readFileSync",[82,946,947],{"class":92},"(file)));\n",[82,949,950,953],{"class":84,"line":128},[82,951,952],{"class":88},"  return",[82,954,955],{"class":92}," { content: [], details: null };\n",[82,957,958],{"class":84,"line":141},[82,959,960],{"class":92},"},\n",[59,962,963,964,967,968,970,971,973],{},"No blocks, nothing printed, not even the newline. An empty text block still prints its empty line, so you only get silence when you ask for it. ",[63,965,966],{},"json: false"," takes ",[63,969,792],{}," away, since bytes followed by JSON is neither. A returned ",[63,972,808],{}," works like on any other command.",[220,975,977],{"id":976},"rows-now-or-json-later","Rows now, or JSON later?",[59,979,980,981,983],{},"Say a command checks 114 addresses, a quarter second each. Sitting silent for half a minute and then dumping everything is a bad look. Better to print each row as it lands and answer with no content. Except now ",[63,982,792],{}," would glue the details under the rows, and that's neither text nor JSON.",[59,985,986,987,990,991,994,995,998,999,1001],{},"So the CLI tells the command what was asked. ",[63,988,989],{},"host"," is a ",[63,992,993],{},"CliHost",", and ",[63,996,997],{},"json"," says whether ",[63,1000,792],{}," came along:",[72,1003,1005],{"className":74,"code":1004,"language":77,"meta":78,"style":78},"import type { CliHost } from \"@agntn\u002Ftools\u002Fcli\";\n\nasync execute({ status }, { host }) {\n  const cli = host as CliHost | undefined;\n  const live = cli?.cli === true && !cli.json;\n  const rows = await check(status, (row) => {\n    if (live) process.stdout.write(`${row}\\n`);\n  });\n  return { content: [], details: { rows } };\n},\n",[63,1006,1007,1023,1027,1037,1064,1091,1115,1139,1144,1151],{"__ignoreMap":78},[82,1008,1009,1011,1014,1017,1019,1021],{"class":84,"line":85},[82,1010,89],{"class":88},[82,1012,1013],{"class":88}," type",[82,1015,1016],{"class":92}," { CliHost } ",[82,1018,96],{"class":88},[82,1020,100],{"class":99},[82,1022,103],{"class":92},[82,1024,1025],{"class":84,"line":106},[82,1026,125],{"emptyLinePlaceholder":124},[82,1028,1029,1032,1034],{"class":84,"line":121},[82,1030,1031],{"class":92},"async ",[82,1033,620],{"class":134},[82,1035,1036],{"class":92},"({ status }, { host }) {\n",[82,1038,1039,1042,1045,1047,1050,1053,1056,1059,1062],{"class":84,"line":128},[82,1040,1041],{"class":88},"  const",[82,1043,1044],{"class":92}," cli ",[82,1046,385],{"class":88},[82,1048,1049],{"class":92}," host ",[82,1051,1052],{"class":88},"as",[82,1054,1055],{"class":134}," CliHost",[82,1057,1058],{"class":88}," |",[82,1060,1061],{"class":134}," undefined",[82,1063,103],{"class":92},[82,1065,1066,1068,1071,1073,1076,1079,1082,1085,1088],{"class":84,"line":141},[82,1067,1041],{"class":88},[82,1069,1070],{"class":92}," live ",[82,1072,385],{"class":88},[82,1074,1075],{"class":92}," cli?.cli ",[82,1077,1078],{"class":88},"===",[82,1080,1081],{"class":92}," true ",[82,1083,1084],{"class":88},"&&",[82,1086,1087],{"class":88}," !",[82,1089,1090],{"class":92},"cli.json;\n",[82,1092,1093,1095,1098,1100,1103,1106,1109,1112],{"class":84,"line":153},[82,1094,1041],{"class":88},[82,1096,1097],{"class":92}," rows ",[82,1099,385],{"class":88},[82,1101,1102],{"class":88}," await",[82,1104,1105],{"class":134}," check",[82,1107,1108],{"class":92},"(status, (row) ",[82,1110,1111],{"class":88},"=>",[82,1113,1114],{"class":92}," {\n",[82,1116,1117,1120,1123,1125,1127,1130,1133,1136],{"class":84,"line":164},[82,1118,1119],{"class":88},"    if",[82,1121,1122],{"class":92}," (live) process.stdout.",[82,1124,933],{"class":134},[82,1126,936],{"class":92},[82,1128,1129],{"class":99},"`${",[82,1131,1132],{"class":99},"row",[82,1134,1135],{"class":99},"}\\n`",[82,1137,1138],{"class":92},");\n",[82,1140,1141],{"class":84,"line":175},[82,1142,1143],{"class":92},"  });\n",[82,1145,1146,1148],{"class":84,"line":181},[82,1147,952],{"class":88},[82,1149,1150],{"class":92}," { content: [], details: { rows } };\n",[82,1152,1153],{"class":84,"line":187},[82,1154,960],{"class":92},[59,1156,1157,1158,1160,1161,1163,1164,1166,1167,1169,1170,1172,1173,1175,1176,1178,1179,1182],{},"Text mode streams. ",[63,1159,792],{}," waits and prints the whole list, valid to the last bracket. Got a pair that can't work together, like bytes on stdout and ",[63,1162,792],{},"? Return an ",[63,1165,808],{}," before the first byte and it lands on stderr with exit code 1. Why check ",[63,1168,244],{}," and not just ",[63,1171,997],{},"? Off the command line ",[63,1174,989],{}," is Pi's own, OMP's or nothing at all. And on ",[63,1177,900],{},", stdout is the protocol. Skip ",[63,1180,1181],{},"progress"," in a command like this. The rows already are the progress, and a progress line would sit right in front of the next one.",[220,1184,1186],{"id":1185},"can-it-guess-what-i-meant","Can it guess what I meant?",[59,1188,1189,1190,1193,1194,1197],{},"If you let it. ",[63,1191,1192],{},"default"," names the command for an empty line. ",[63,1195,1196],{},"fallback"," takes any first word that isn't a command:",[72,1199,1201],{"className":74,"code":1200,"language":77,"meta":78,"style":78},"fallback: \"slug\",\n",[63,1202,1203],{"__ignoreMap":78},[82,1204,1205,1208,1211],{"class":84,"line":85},[82,1206,1207],{"class":92},"fallback: ",[82,1209,1210],{"class":99},"\"slug\"",[82,1212,150],{"class":92},[72,1214,1217],{"className":1215,"code":1216,"filename":203,"language":204,"meta":78},[201],"$ slugs \"Hello, World\"\nhello-world\n",[63,1218,1216],{"__ignoreMap":78},[59,1220,1221,1222,1224,1225,1228],{},"Just remember it's greedy. Leave ",[63,1223,900],{}," out and ",[63,1226,1227],{},"slugs mcp"," slugs the word \"mcp\". Correct, and completely useless.",[59,1230,1231,1232,1235,1236,234,1238,234,1240,267,1243,1246],{},"A flag up front goes there too, so ",[63,1233,1234],{},"slugs --separator _ \"Hello, World\""," works without the command name. Flags first is how half of us type anyway. Only ",[63,1237,453],{},[63,1239,770],{},[63,1241,1242],{},"--version",[63,1244,1245],{},"-v"," stay with the root, which answers them itself.",[215,1248,1250],{"id":1249},"an-mcp-server-on-the-side","An MCP server on the side",[59,1252,1253,1256,1257,1259,1260,1263],{},[63,1254,1255],{},"mcp: true"," adds an ",[63,1258,900],{}," command. It's ",[63,1261,1262],{},"createMcpServer"," over stdio, and the server introduces itself with the CLI's name, version and help line:",[72,1265,1269],{"className":1266,"code":1267,"filename":1268,"language":997,"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",[63,1270,1271,1276,1284,1314,1319],{"__ignoreMap":78},[82,1272,1273],{"class":84,"line":85},[82,1274,1275],{"class":92},"{\n",[82,1277,1278,1281],{"class":84,"line":106},[82,1279,1280],{"class":134},"  \"mcpServers\"",[82,1282,1283],{"class":92},": {\n",[82,1285,1286,1289,1292,1295,1298,1300,1302,1305,1308,1311],{"class":84,"line":121},[82,1287,1288],{"class":134},"    \"slugs\"",[82,1290,1291],{"class":92},": { ",[82,1293,1294],{"class":134},"\"command\"",[82,1296,1297],{"class":92},": ",[82,1299,147],{"class":99},[82,1301,234],{"class":92},[82,1303,1304],{"class":134},"\"args\"",[82,1306,1307],{"class":92},": [",[82,1309,1310],{"class":99},"\"mcp\"",[82,1312,1313],{"class":92},"] }\n",[82,1315,1316],{"class":84,"line":128},[82,1317,1318],{"class":92},"  }\n",[82,1320,1321],{"class":84,"line":141},[82,1322,1323],{"class":92},"}\n",[59,1325,1326,1327,1329,1330,439],{},"A help line makes a sad connector card, though. Got a real pitch and an icon? Hand ",[63,1328,900],{}," the whole server info instead of ",[63,1331,1332],{},"true",[72,1334,1336],{"className":74,"code":1335,"filename":76,"language":77,"meta":78,"style":78},"import { serverInfo } from \".\u002Fserver-info.ts\";\n\nawait runCli({\n  name: \"slugs\",\n  version: \"1.0.0\",\n  description: \"Slugs from titles\",\n  tools: [slugTool],\n  mcp: serverInfo,\n});\n",[63,1337,1338,1352,1356,1364,1372,1380,1388,1392,1397],{"__ignoreMap":78},[82,1339,1340,1342,1345,1347,1350],{"class":84,"line":85},[82,1341,89],{"class":88},[82,1343,1344],{"class":92}," { serverInfo } ",[82,1346,96],{"class":88},[82,1348,1349],{"class":99}," \".\u002Fserver-info.ts\"",[82,1351,103],{"class":92},[82,1353,1354],{"class":84,"line":106},[82,1355,125],{"emptyLinePlaceholder":124},[82,1357,1358,1360,1362],{"class":84,"line":121},[82,1359,131],{"class":88},[82,1361,135],{"class":134},[82,1363,138],{"class":92},[82,1365,1366,1368,1370],{"class":84,"line":128},[82,1367,144],{"class":92},[82,1369,147],{"class":99},[82,1371,150],{"class":92},[82,1373,1374,1376,1378],{"class":84,"line":141},[82,1375,156],{"class":92},[82,1377,159],{"class":99},[82,1379,150],{"class":92},[82,1381,1382,1384,1386],{"class":84,"line":153},[82,1383,167],{"class":92},[82,1385,170],{"class":99},[82,1387,150],{"class":92},[82,1389,1390],{"class":84,"line":164},[82,1391,178],{"class":92},[82,1393,1394],{"class":84,"line":175},[82,1395,1396],{"class":92},"  mcp: serverInfo,\n",[82,1398,1399],{"class":84,"line":181},[82,1400,190],{"class":92},[59,1402,1403,1404,1410,1411,1413,1414,1417,1418,1420,1421,1424,1425,213],{},"It's the same ",[1405,1406,1407],"a",{"href":37},[63,1408,1409],{},"McpServerInfo"," that ",[63,1412,1262],{}," takes. So the ",[63,1415,1416],{},"\u002Fmcp"," on your site and ",[63,1419,1227],{}," can share one object and say hello the same way. The CLI's own ",[63,1422,1423],{},"description"," stays where it belongs, in ",[63,1426,453],{},[59,1428,1429,1430,1433,1434,1436,1437,1440,1441,213],{},"One binary, two jobs. The server serves ",[63,1431,1432],{},"tools"," only, your own ",[63,1435,844],{}," stay on the command line. It needs ",[63,1438,1439],{},"@modelcontextprotocol\u002Fserver"," installed, and loads it only when someone actually runs ",[63,1442,900],{},[59,1444,1445,1446,1449,1450,1453,1454,1456,1457,1460],{},"Progress shows up on both sides. On a terminal, each ",[63,1447,1448],{},"progress?.()"," line takes over one line of stderr, with ",[63,1451,1452],{},"(3\u002F10)"," when you pass a total. It's wiped before the answer prints, so the scrollback keeps the answer and nothing else. Is stderr a pipe or a file? Then there's no line at all, since nobody wants \"second 7 of 10\" in a log. Through ",[63,1455,1227],{}," it's a real MCP server, so a client that sends a ",[63,1458,1459],{},"progressToken"," gets every line.",[1462,1463,1464],"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":1466},[1467,1475,1476,1481],{"id":217,"depth":106,"text":218,"children":1468},[1469,1470,1471,1472,1473,1474],{"id":222,"depth":121,"text":223},{"id":259,"depth":121,"text":260},{"id":357,"depth":121,"text":358},{"id":478,"depth":121,"text":479},{"id":632,"depth":121,"text":633},{"id":759,"depth":121,"text":760},{"id":785,"depth":106,"text":786},{"id":829,"depth":106,"text":830,"children":1477},[1478,1479,1480],{"id":904,"depth":121,"text":905},{"id":976,"depth":121,"text":977},{"id":1185,"depth":121,"text":1186},{"id":1249,"depth":106,"text":1250},"Turn the same tool list into a command line. Flags come from the schema and MCP rides along","md",null,{"icon":1486},"i-lucide-square-terminal",{"title":20},{"title":1489,"description":1482},"A command line from the tools you already have","udgThSitD6FVroUT-VUpwd5yRPHjyxO4FiyNxyQOcd8",[1492,1494],{"title":16,"path":17,"stem":18,"description":1493,"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":1495,"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",1791905429176]