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