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