[{"data":1,"prerenderedAt":768},["ShallowReactive",2],{"navigation_docs":3,"-hosts-pi":52,"-hosts-pi-surround":710,"host-views":715},[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":704,"extension":705,"links":706,"meta":707,"navigation":133,"path":41,"seo":708,"stem":42,"__hash__":709},"docs\u002F2.hosts\u002F02.pi.md",{"type":55,"value":56,"toc":696},"minimark",[57,61,175,206,211,222,225,244,255,259,262,269,346,353,364,374,381,385,402,480,502,515,519,530,541,594,607,618,622,641,661,664,668,692],[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},"questions-from-the-tool","Questions from the tool",[176,386,387,389,390,397,398,401],{},[70,388,267],{}," asks before the call. ",[391,392,394],"a",{"href":393},"\u002Fguide\u002Fdefining-tools#ask-the-person-not-the-model",[70,395,396],{},"ask"," asks from inside it, once the tool knows what to ask. Pi has no form dialog, so the adapter walks the fields one ",[70,399,400],{},"ctx.ui"," dialog at a time:",[403,404,405,418],"table",{},[406,407,408],"thead",{},[409,410,411,415],"tr",{},[412,413,414],"th",{},"Field",[412,416,417],{},"Dialog",[419,420,421,435,452,469],"tbody",{},[409,422,423,429],{},[424,425,426],"td",{},[70,427,428],{},"Type.Enum",[424,430,431,434],{},[70,432,433],{},"select"," with the values",[409,436,437,440],{},[424,438,439],{},"Array of an enum",[424,441,442,444,445,448,449],{},[70,443,433],{}," with ",[70,446,447],{},"[x]"," toggles and ",[70,450,451],{},"Done",[409,453,454,459],{},[424,455,456],{},[70,457,458],{},"Type.Boolean",[424,460,461,444,463,197,466],{},[70,462,433],{},[70,464,465],{},"Yes",[70,467,468],{},"No",[409,470,471,474],{},[424,472,473],{},"Anything else",[424,475,476,479],{},[70,477,478],{},"input",", read as a number for a numeric field",[176,481,482,483,486,487,489,490,493,494,497,498,501],{},"An optional field with a list gets a ",[70,484,485],{},"(skip)"," line, toggles included. An empty ",[70,488,478],{}," takes the default, or leaves an optional field out. Typed ",[70,491,492],{},"0"," into a field with ",[70,495,496],{},"minimum: 1","? Pi shows a warning with the reason and asks again. Escape on any dialog closes the whole form, and the tool reads ",[70,499,500],{},"cancel",".",[176,503,504,505,508,509,511,512,514],{},"Every dialog gets the call's abort signal. With ",[70,506,507],{},"ctx.hasUI"," off, as in ",[70,510,358],{},", there's no ",[70,513,396],{}," at all. Nobody to ask, nothing to wait for.",[207,516,518],{"id":517},"call-lines","Call lines",[176,520,521,522,525,526,529],{},"Left alone, Pi draws a call as the tool name and a ",[70,523,524],{},"key=value"," dump of every argument. Fine for ",[70,527,528],{},"read",". For a tool whose one interesting argument is a long URL? Mostly noise.",[176,531,532,533,536,537,540],{},"Hand the adapter a ",[70,534,535],{},"describeCall"," and Pi's ",[70,538,539],{},"Text",", and the line becomes the title plus your summary, styled like Pi's own tools:",[62,542,544],{"className":64,"code":543,"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,545,546,560,564,570,575,590],{"__ignoreMap":68},[73,547,548,550,553,555,558],{"class":75,"line":76},[73,549,80],{"class":79},[73,551,552],{"class":86}," { Text } ",[73,554,90],{"class":79},[73,556,557],{"class":93}," \"@earendil-works\u002Fpi-tui\"",[73,559,97],{"class":86},[73,561,562],{"class":75,"line":100},[73,563,134],{"emptyLinePlaceholder":133},[73,565,566,568],{"class":75,"line":115},[73,567,234],{"class":155},[73,569,280],{"class":86},[73,571,572],{"class":75,"line":130},[73,573,574],{"class":86},"  Text,\n",[73,576,577,580,582,585,587],{"class":75,"line":137},[73,578,579],{"class":86},"  renderers: { text_slug: { ",[73,581,535],{"class":155},[73,583,584],{"class":86},": (args) ",[73,586,296],{"class":79},[73,588,589],{"class":86}," args.text } },\n",[73,591,592],{"class":75,"line":162},[73,593,345],{"class":86},[176,595,596,597,600,601,603,604,606],{},"Pi resolves ",[70,598,599],{},"@earendil-works\u002Fpi-tui"," for every extension it loads, so the import works at runtime with nothing installed. The summary goes through ",[70,602,369],{}," first. A newline in a model's argument can't forge a line. Forget ",[70,605,539],{}," and registration throws, which beats a summary that never shows up.",[176,608,609,610,612,613,617],{},"It's the same ",[70,611,535],{}," ",[391,614,616],{"href":615},"\u002Fhosts\u002Fomp#status-lines","OMP takes",". One function, two hosts.",[207,619,621],{"id":620},"renderers","Renderers",[176,623,624,625,197,628,631,632,634,635,637,638,640],{},"Pass Pi's own ",[70,626,627],{},"renderCall",[70,629,630],{},"renderResult"," per tool through ",[70,633,620],{},". They reach Pi unchanged, and a ",[70,636,627],{}," beats ",[70,639,535],{}," for the same tool:",[62,642,644],{"className":64,"code":643,"language":67,"meta":68,"style":68},"registerPiTools(pi, tools, {\n  renderers: { books_text: { renderResult: preview } },\n});\n",[70,645,646,652,657],{"__ignoreMap":68},[73,647,648,650],{"class":75,"line":76},[73,649,234],{"class":155},[73,651,280],{"class":86},[73,653,654],{"class":75,"line":100},[73,655,656],{"class":86},"  renderers: { books_text: { renderResult: preview } },\n",[73,658,659],{"class":75,"line":115},[73,660,345],{"class":86},[176,662,663],{},"Handy when a tool returns a lot of text and you want the collapsed row to stay short.",[207,665,667],{"id":666},"progress","Progress",[176,669,670,671,673,674,677,678,681,682,684,685,688,689,691],{},"A ",[70,672,666],{}," line reaches Pi through ",[70,675,676],{},"onUpdate",". It's a partial result: one text block, ",[70,679,680],{},"details: {}",". Pi shows it while the call runs. Your own ",[70,683,630],{}," gets it too, with ",[70,686,687],{},"isPartial"," set. Reading ",[70,690,249],{}," in there? Check that flag first.",[693,694,695],"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":697},[698,699,700,701,702,703],{"id":209,"depth":100,"text":210},{"id":257,"depth":100,"text":258},{"id":383,"depth":100,"text":384},{"id":517,"depth":100,"text":518},{"id":620,"depth":100,"text":621},{"id":666,"depth":100,"text":667},"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":704},"fVPGqdvrWAeiSbnhOZpYAeE-s74ftH0nXb8cc2HUcC8",[711,713],{"title":36,"path":37,"stem":38,"description":712,"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":714,"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",[716,732,744,756],{"key":717,"rows":718,"registration":729,"fields":730},"mcp",[719,721,724,727],{"label":189,"value":720},"Text Slug",{"label":722,"value":723,"accent":133},"hints","readOnly, idempotent",{"label":725,"value":726},"inputSchema","text, separator",{"label":728,"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":725,"effect":731,"prompt":706,"run":706},"annotations",{"key":60,"rows":733,"registration":741,"fields":742},[734,735,738,739],{"label":185,"value":720},{"label":736,"value":737,"accent":133},"prompt","snippet, 1 rule",{"label":204,"value":726},{"label":728,"value":740},"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":706,"prompt":196,"run":743},"execute",{"key":745,"rows":746,"registration":754,"fields":755},"omp",[747,748,750,752],{"label":185,"value":720},{"label":749,"value":528,"accent":133},"approval",{"label":204,"value":751},"Type.Unsafe(json)",{"label":728,"value":753},"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":749,"prompt":706,"run":743},{"key":757,"rows":758,"registration":766,"fields":767},"ai",[759,760,762,765],{"label":189,"value":720},{"label":725,"value":761,"accent":133},"jsonSchema()",{"label":763,"value":764},"output","details + text",{"label":728,"value":740},"{\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  \"toModelOutput\": \"[function]\"\n}",{"name":706,"label":189,"schema":725,"effect":706,"prompt":706,"run":743},1791593263862]