[{"data":1,"prerenderedAt":630},["ShallowReactive",2],{"navigation_docs":3,"-hosts-pi":48,"-hosts-pi-surround":572,"host-views":577},[4,24],{"title":5,"path":6,"stem":7,"children":8,"icon":23},"Guide","\u002Fguide","1.guide\u002F01.index",[9,11,15,19],{"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},"Moving a Package Over","\u002Fguide\u002Fmigrating","1.guide\u002F04.migrating","i-lucide-book-open",{"title":25,"path":26,"stem":27,"children":28,"icon":47},"Hosts","\u002Fhosts","2.hosts\u002F00.index",[29,31,35,39,43],{"title":30,"path":26,"stem":27},"Every host",{"title":32,"path":33,"stem":34},"MCP","\u002Fhosts\u002Fmcp","2.hosts\u002F01.mcp",{"title":36,"path":37,"stem":38},"Pi","\u002Fhosts\u002Fpi","2.hosts\u002F02.pi",{"title":40,"path":41,"stem":42},"OMP","\u002Fhosts\u002Fomp","2.hosts\u002F03.omp",{"title":44,"path":45,"stem":46},"AI SDK","\u002Fhosts\u002Fai-sdk","2.hosts\u002F04.ai-sdk","i-lucide-table",{"id":49,"title":36,"body":50,"description":566,"extension":567,"links":568,"meta":569,"navigation":129,"path":37,"seo":570,"stem":38,"__hash__":571},"docs\u002F2.hosts\u002F02.pi.md",{"type":51,"value":52,"toc":559},"minimark",[53,57,171,202,207,218,221,240,251,255,258,265,342,349,360,370,377,381,392,403,456,469,481,485,504,524,527,531,555],[54,55],"host-facts",{"name":56},"pi",[58,59,65],"pre",{"className":60,"code":61,"filename":62,"language":63,"meta":64,"style":64},"language-ts shiki shiki-themes tools tools tools","import type { ExtensionAPI } from \"@earendil-works\u002Fpi-coding-agent\";\nimport { registerPiTools } from \"@agntn\u002Ftools\u002Fpi\";\nimport { tools } from \"..\u002F..\u002F..\u002Fsrc\u002Ftools.ts\";\n\nexport default function (pi: ExtensionAPI) {\n  registerPiTools(pi, tools);\n}\n","packages\u002Fpi\u002Fextensions\u002Fmypackage.ts","ts","",[66,67,68,94,109,124,131,156,165],"code",{"__ignoreMap":64},[69,70,73,77,80,84,87,91],"span",{"class":71,"line":72},"line",1,[69,74,76],{"class":75},"skH_V","import",[69,78,79],{"class":75}," type",[69,81,83],{"class":82},"s38Sx"," { ExtensionAPI } ",[69,85,86],{"class":75},"from",[69,88,90],{"class":89},"shU9J"," \"@earendil-works\u002Fpi-coding-agent\"",[69,92,93],{"class":82},";\n",[69,95,97,99,102,104,107],{"class":71,"line":96},2,[69,98,76],{"class":75},[69,100,101],{"class":82}," { registerPiTools } ",[69,103,86],{"class":75},[69,105,106],{"class":89}," \"@agntn\u002Ftools\u002Fpi\"",[69,108,93],{"class":82},[69,110,112,114,117,119,122],{"class":71,"line":111},3,[69,113,76],{"class":75},[69,115,116],{"class":82}," { tools } ",[69,118,86],{"class":75},[69,120,121],{"class":89}," \"..\u002F..\u002F..\u002Fsrc\u002Ftools.ts\"",[69,123,93],{"class":82},[69,125,127],{"class":71,"line":126},4,[69,128,130],{"emptyLinePlaceholder":129},true,"\n",[69,132,134,137,140,143,146,149,153],{"class":71,"line":133},5,[69,135,136],{"class":75},"export",[69,138,139],{"class":75}," default",[69,141,142],{"class":75}," function",[69,144,145],{"class":82}," (pi",[69,147,148],{"class":75},":",[69,150,152],{"class":151},"sK71F"," ExtensionAPI",[69,154,155],{"class":82},") {\n",[69,157,159,162],{"class":71,"line":158},6,[69,160,161],{"class":151},"  registerPiTools",[69,163,164],{"class":82},"(pi, tools);\n",[69,166,168],{"class":71,"line":167},7,[69,169,170],{"class":82},"}\n",[172,173,174,175,178,179,182,183,186,187,178,190,193,194,197,198,201],"p",{},"Pi gets ",[66,176,177],{},"name",", ",[66,180,181],{},"label"," from ",[66,184,185],{},"title",", the ",[66,188,189],{},"description",[66,191,192],{},"promptSnippet"," and ",[66,195,196],{},"promptGuidelines"," when you set them, and the schema as ",[66,199,200],{},"parameters",". Same JSON Schema every other host gets. Pi's own validator checks it, then the core checks it again, because a second check costs nothing and a missing one costs an afternoon.",[203,204,206],"h2",{"id":205},"failures","Failures",[172,208,209,210,213,214,217],{},"Up to 0.98, Pi reads a returned object as a success no matter what's in it. A tool that returns ",[66,211,212],{},"isError: true"," would look fine to the model. So by default the adapter throws the result's text when ",[66,215,216],{},"isError"," is set, and Pi records an error.",[172,219,220],{},"On Pi 0.99 or newer you can keep the result as it is:",[58,222,224],{"className":60,"code":223,"language":63,"meta":64,"style":64},"registerPiTools(pi, tools, { failures: \"return\" });\n",[66,225,226],{"__ignoreMap":64},[69,227,228,231,234,237],{"class":71,"line":72},[69,229,230],{"class":151},"registerPiTools",[69,232,233],{"class":82},"(pi, tools, { failures: ",[69,235,236],{"class":89},"\"return\"",[69,238,239],{"class":82}," });\n",[172,241,242,243,246,247,250],{},"That keeps ",[66,244,245],{},"details"," on a failure too. Only do it with a peer range of ",[66,248,249],{},">=0.99.0",", since Pi doesn't check peer ranges when it installs a package.",[203,252,254],{"id":253},"asking-first","Asking first",[172,256,257],{},"Some calls shouldn't run just because the model asked. Opening a pull request, say. Or overwriting release notes?",[172,259,260,261,264],{},"Give the adapter a ",[66,262,263],{},"confirm"," builder per tool, and Pi asks the user before the call runs:",[58,266,268],{"className":60,"code":267,"language":63,"meta":64,"style":64},"registerPiTools(pi, tools, {\n  confirm: {\n    notes_publish: (input) => ({\n      title: \"Publish note?\",\n      message: `Title  ${String(input.title)}`,\n    }),\n  },\n});\n",[66,269,270,277,282,296,307,326,331,336],{"__ignoreMap":64},[69,271,272,274],{"class":71,"line":72},[69,273,230],{"class":151},[69,275,276],{"class":82},"(pi, tools, {\n",[69,278,279],{"class":71,"line":96},[69,280,281],{"class":82},"  confirm: {\n",[69,283,284,287,290,293],{"class":71,"line":111},[69,285,286],{"class":151},"    notes_publish",[69,288,289],{"class":82},": (input) ",[69,291,292],{"class":75},"=>",[69,294,295],{"class":82}," ({\n",[69,297,298,301,304],{"class":71,"line":126},[69,299,300],{"class":82},"      title: ",[69,302,303],{"class":89},"\"Publish note?\"",[69,305,306],{"class":82},",\n",[69,308,309,312,315,318,321,324],{"class":71,"line":133},[69,310,311],{"class":82},"      message: ",[69,313,314],{"class":89},"`Title  ${",[69,316,317],{"class":151},"String",[69,319,320],{"class":89},"(input.title)",[69,322,323],{"class":89},"}`",[69,325,306],{"class":82},[69,327,328],{"class":71,"line":158},[69,329,330],{"class":82},"    }),\n",[69,332,333],{"class":71,"line":167},[69,334,335],{"class":82},"  },\n",[69,337,339],{"class":71,"line":338},8,[69,340,341],{"class":82},"});\n",[172,343,344,345,348],{},"The arguments pass validation first, so the builder never sees junk. Then the dialog opens through ",[66,346,347],{},"ctx.ui.confirm"," with the call's abort signal. A yes runs the tool. A no throws, and the model reads that it shouldn't retry unless the user asks again.",[172,350,351,352,355,356,359],{},"No UI, no write. In ",[66,353,354],{},"pi -p"," there's nobody to ask, so the call throws instead of running unasked. Return ",[66,357,358],{},"undefined"," from the builder to skip the question for one call, a dry run for example.",[172,361,362,363,366,367,369],{},"The title goes through ",[66,364,365],{},"sanitizeLine",". The message keeps its lines and tabs, but escape sequences and bidi tricks don't make it to the dialog. A model can't paint its own yes button. A name in ",[66,368,263],{}," that isn't in the tool list throws at registration. A typo there would drop the check without a sound, the worst kind of quiet.",[172,371,372,373,376],{},"OMP doesn't need any of this. Its ",[66,374,375],{},"approval: \"write\""," already stands in front of every write.",[203,378,380],{"id":379},"call-lines","Call lines",[172,382,383,384,387,388,391],{},"Left alone, Pi draws a call as the tool name and a ",[66,385,386],{},"key=value"," dump of every argument. Fine for ",[66,389,390],{},"read",". For a tool whose one interesting argument is a long URL? Mostly noise.",[172,393,394,395,398,399,402],{},"Hand the adapter a ",[66,396,397],{},"describeCall"," and Pi's ",[66,400,401],{},"Text",", and the line becomes the title plus your summary, styled like Pi's own tools:",[58,404,406],{"className":60,"code":405,"language":63,"meta":64,"style":64},"import { Text } from \"@earendil-works\u002Fpi-tui\";\n\nregisterPiTools(pi, tools, {\n  Text,\n  renderers: { text_slug: { describeCall: (args) => args.text } },\n});\n",[66,407,408,422,426,432,437,452],{"__ignoreMap":64},[69,409,410,412,415,417,420],{"class":71,"line":72},[69,411,76],{"class":75},[69,413,414],{"class":82}," { Text } ",[69,416,86],{"class":75},[69,418,419],{"class":89}," \"@earendil-works\u002Fpi-tui\"",[69,421,93],{"class":82},[69,423,424],{"class":71,"line":96},[69,425,130],{"emptyLinePlaceholder":129},[69,427,428,430],{"class":71,"line":111},[69,429,230],{"class":151},[69,431,276],{"class":82},[69,433,434],{"class":71,"line":126},[69,435,436],{"class":82},"  Text,\n",[69,438,439,442,444,447,449],{"class":71,"line":133},[69,440,441],{"class":82},"  renderers: { text_slug: { ",[69,443,397],{"class":151},[69,445,446],{"class":82},": (args) ",[69,448,292],{"class":75},[69,450,451],{"class":82}," args.text } },\n",[69,453,454],{"class":71,"line":158},[69,455,341],{"class":82},[172,457,458,459,462,463,465,466,468],{},"Pi resolves ",[66,460,461],{},"@earendil-works\u002Fpi-tui"," for every extension it loads, so the import works at runtime with nothing installed. The summary goes through ",[66,464,365],{}," first. A newline in a model's argument can't forge a line. Forget ",[66,467,401],{}," and registration throws, which beats a summary that never shows up.",[172,470,471,472,474,475,480],{},"It's the same ",[66,473,397],{}," ",[476,477,479],"a",{"href":478},"\u002Fhosts\u002Fomp#status-lines","OMP takes",". One function, two hosts.",[203,482,484],{"id":483},"renderers","Renderers",[172,486,487,488,193,491,494,495,497,498,500,501,503],{},"Pass Pi's own ",[66,489,490],{},"renderCall",[66,492,493],{},"renderResult"," per tool through ",[66,496,483],{},". They reach Pi unchanged, and a ",[66,499,490],{}," beats ",[66,502,397],{}," for the same tool:",[58,505,507],{"className":60,"code":506,"language":63,"meta":64,"style":64},"registerPiTools(pi, tools, {\n  renderers: { books_text: { renderResult: preview } },\n});\n",[66,508,509,515,520],{"__ignoreMap":64},[69,510,511,513],{"class":71,"line":72},[69,512,230],{"class":151},[69,514,276],{"class":82},[69,516,517],{"class":71,"line":96},[69,518,519],{"class":82},"  renderers: { books_text: { renderResult: preview } },\n",[69,521,522],{"class":71,"line":111},[69,523,341],{"class":82},[172,525,526],{},"Handy when a tool returns a lot of text and you want the collapsed row to stay short.",[203,528,530],{"id":529},"progress","Progress",[172,532,533,534,536,537,540,541,544,545,547,548,551,552,554],{},"A ",[66,535,529],{}," line reaches Pi through ",[66,538,539],{},"onUpdate",". It's a partial result: one text block, ",[66,542,543],{},"details: {}",". Pi shows it while the call runs. Your own ",[66,546,493],{}," gets it too, with ",[66,549,550],{},"isPartial"," set. Reading ",[66,553,245],{}," in there? Check that flag first.",[556,557,558],"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);}",{"title":64,"searchDepth":96,"depth":96,"links":560},[561,562,563,564,565],{"id":205,"depth":96,"text":206},{"id":253,"depth":96,"text":254},{"id":379,"depth":96,"text":380},{"id":483,"depth":96,"text":484},{"id":529,"depth":96,"text":530},"registerPiTools puts every tool on Pi with its prompt snippet and guidelines. And throws on failure because older Pi only listens to throws","md",null,{},{"title":36,"description":566},"BHQqDHE5LTo6vryIyOL0HZGd1MkYds2iw8jUD6lQaNc",[573,575],{"title":32,"path":33,"stem":34,"description":574,"children":-1},"createMcpServer builds an MCP server on SDK v2 for any list of tools. With the error handling the SDK's own McpServer gets wrong",{"title":40,"path":41,"stem":42,"description":576,"children":-1},"registerOmpTools puts every tool on OMP with an approval tier and a status line. Built around the TypeBox rewrite OMP does behind your back",[578,594,606,618],{"key":579,"rows":580,"registration":591,"fields":592},"mcp",[581,583,586,589],{"label":185,"value":582},"Text Slug",{"label":584,"value":585,"accent":129},"hints","readOnly, idempotent",{"label":587,"value":588},"inputSchema","text, separator",{"label":590,"value":212},"failure","{\n  \"name\": \"text_slug\",\n  \"title\": \"Text Slug\",\n  \"description\": \"Turn a title into a lowercase URL slug. Letters and digits stay, accents lose their marks, the rest becomes the separator.\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"text\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 200,\n        \"pattern\": \"\\\\S\",\n        \"description\": \"Title to slug\"\n      },\n      \"separator\": {\n        \"enum\": [\n          \"-\",\n          \"_\",\n          \".\"\n        ],\n        \"description\": \"Joins the words. '-' when omitted\"\n      }\n    },\n    \"required\": [\n      \"text\"\n    ],\n    \"additionalProperties\": false\n  },\n  \"annotations\": {\n    \"title\": \"Text Slug\",\n    \"readOnlyHint\": true,\n    \"destructiveHint\": false,\n    \"idempotentHint\": true,\n    \"openWorldHint\": false\n  }\n}",{"name":177,"label":185,"schema":587,"effect":593,"prompt":568,"run":568},"annotations",{"key":56,"rows":595,"registration":603,"fields":604},[596,597,600,601],{"label":181,"value":582},{"label":598,"value":599,"accent":129},"prompt","snippet, 1 rule",{"label":200,"value":588},{"label":590,"value":602},"thrown","{\n  \"name\": \"text_slug\",\n  \"label\": \"Text Slug\",\n  \"description\": \"Turn a title into a lowercase URL slug. Letters and digits stay, accents lose their marks, the rest becomes the separator.\",\n  \"promptSnippet\": \"Use text_slug to turn a title into a URL slug.\",\n  \"promptGuidelines\": [\n    \"Pass separator '_' for identifiers. The default '-' is for URLs.\"\n  ],\n  \"parameters\": {\n    \"type\": \"object\",\n    \"required\": [\n      \"text\"\n    ],\n    \"properties\": {\n      \"text\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 200,\n        \"pattern\": \"\\\\S\",\n        \"description\": \"Title to slug\"\n      },\n      \"separator\": {\n        \"enum\": [\n          \"-\",\n          \"_\",\n          \".\"\n        ],\n        \"description\": \"Joins the words. '-' when omitted\"\n      }\n    },\n    \"additionalProperties\": false\n  },\n  \"execute\": \"[function]\"\n}",{"name":177,"label":181,"schema":200,"effect":568,"prompt":192,"run":605},"execute",{"key":607,"rows":608,"registration":616,"fields":617},"omp",[609,610,612,614],{"label":181,"value":582},{"label":611,"value":390,"accent":129},"approval",{"label":200,"value":613},"Type.Unsafe(json)",{"label":590,"value":615},"isError or thrown","{\n  \"name\": \"text_slug\",\n  \"label\": \"Text Slug\",\n  \"description\": \"Turn a title into a lowercase URL slug. Letters and digits stay, accents lose their marks, the rest becomes the separator.\",\n  \"parameters\": {\n    \"unsafe\": {\n      \"type\": \"object\",\n      \"required\": [\n        \"text\"\n      ],\n      \"properties\": {\n        \"text\": {\n          \"type\": \"string\",\n          \"minLength\": 1,\n          \"maxLength\": 200,\n          \"pattern\": \"\\\\S\",\n          \"description\": \"Title to slug\"\n        },\n        \"separator\": {\n          \"enum\": [\n            \"-\",\n            \"_\",\n            \".\"\n          ],\n          \"description\": \"Joins the words. '-' when omitted\"\n        }\n      },\n      \"additionalProperties\": false\n    }\n  },\n  \"approval\": \"read\",\n  \"execute\": \"[function]\",\n  \"renderCall\": \"[function]\",\n  \"renderResult\": \"[function]\"\n}",{"name":177,"label":181,"schema":200,"effect":611,"prompt":568,"run":605},{"key":619,"rows":620,"registration":628,"fields":629},"ai",[621,622,624,627],{"label":185,"value":582},{"label":587,"value":623,"accent":129},"jsonSchema()",{"label":625,"value":626},"output","details + text",{"label":590,"value":602},"{\n  \"title\": \"Text Slug\",\n  \"description\": \"Turn a title into a lowercase URL slug. Letters and digits stay, accents lose their marks, the rest becomes the separator.\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"required\": [\n      \"text\"\n    ],\n    \"properties\": {\n      \"text\": {\n        \"type\": \"string\",\n        \"minLength\": 1,\n        \"maxLength\": 200,\n        \"pattern\": \"\\\\S\",\n        \"description\": \"Title to slug\"\n      },\n      \"separator\": {\n        \"enum\": [\n          \"-\",\n          \"_\",\n          \".\"\n        ],\n        \"description\": \"Joins the words. '-' when omitted\"\n      }\n    },\n    \"additionalProperties\": false\n  },\n  \"execute\": \"[function]\"\n}",{"name":568,"label":185,"schema":587,"effect":568,"prompt":568,"run":605},1791280825641]