Your adapter forwards each method to your app. Add any auth headers it needs.
- A value like \${WEATHER_TOKEN} is a secret placeholder: the operator who installs the app
- supplies it at install time (from their environment or a local secrets file) — it is never stored in the published app.
-
-
Base URL ${info('The production endpoint. Baked in as the default; operators can override.')}
-
Each method is one call an agent can make. Give it a clear name, the backend route, a latency class, a description, and its parameters. The live preview shows exactly what agents will see and run.
Your adapter forwards each method to your app. Add any auth headers it needs.
+ A value like \${WEATHER_TOKEN} is a secret placeholder: the operator who installs the app
+ supplies it at install time (from their environment or a local secrets file) — it is never stored in the published app.
+
+
Base URL ${info('The production endpoint. Baked in as the default; operators can override.')}
+
We generate, sign, and verify an adapter that runs your command. The command must already be
+ installed on the operator's host. The child runs with a scrubbed environment — only the variables you list below
+ (plus PATH/HOME/locale) are passed through. The app ships with a proc.exec grant scoped to exactly this command, and installs guarded via the reviewed catalogue.
+
+
Command ${info('The base argv the adapter runs. Comma-separated, e.g. gh —or— python, -m, tool. Each method appends its own args.')}
+
+
Env passthrough (optional) ${info('Host environment variables the CLI may see, comma-separated. Everything else is scrubbed from the child.')}
+
Path ${info('Backend route. GET → params become the query string; POST → JSON body.')}
`;
+}
+// The CLI route lives in a full-width row below the name/latency grid.
+function cliRouteRow(m){
+ const pass = !!m.cli.passthrough;
+ const enumerated = `Arguments ${info('Comma-separated argv appended to the command. Use ${param} to insert a payload field, e.g. current, --lat, ${lat}')}
+
+ Also append each parameter as --name value`;
+ const passthru = `
Every subcommand is reachable; the caller supplies the argv. Call it as {"args":["…"]} — no baked arguments.
`;
+ return `
+ Passthrough — front the whole CLI (translate any <cli> <args>)
+ ${pass ? passthru : enumerated}
+
Method name ${info('Prefixed with your namespace, e.g. '+esc(ns()||'app')+'.search')}
-
Verb
-
Path ${info('Backend route. GET → params become the query string; POST → JSON body.')}
+ ${routeCells}
Latency ${info('How long a typical call takes — agents use it to pick the cheapest method that fits. Fast: under 5s · Medium: up to 15s · Slow: up to 1 min.')}
+ ${cli ? cliRouteRow(m) : ''}
Description ${info('Shown in the help doc agents read. Be specific about what it returns.')}
@@ -451,13 +499,19 @@ function wire() {
bindVal('f-version',v=>state.version=v); bindVal('f-description',v=>state.description=v);
}
if (cur==='Backend'){
- bindVal('f-baseurl',v=>state.backend.base_url=v);
- document.getElementById('add-header').onclick=()=>{ state.backend.headers.push(blankHeader()); save(); render(); };
- document.querySelectorAll('#headers .prow').forEach(rowEl=>{ const i=+rowEl.dataset.hi;
- rowEl.querySelector('.h-name').addEventListener('input',e=>{state.backend.headers[i].name=e.target.value;save();});
- rowEl.querySelector('.h-value').addEventListener('input',e=>{state.backend.headers[i].value=e.target.value;save();});
- rowEl.querySelector('.h-del').onclick=()=>{ state.backend.headers.splice(i,1); if(!state.backend.headers.length)state.backend.headers.push(blankHeader()); save(); render(); };
- });
+ document.querySelectorAll('input[name="apptype"]').forEach(r=>{ r.addEventListener('change',()=>{ if(r.checked && state.app_type!==r.value){ state.app_type=r.value; save(); render(); schedulePreview(); } }); });
+ if (state.app_type==='cli'){
+ bindVal('f-command',v=>state.backend.command=v);
+ bindVal('f-envpass',v=>state.backend.env_passthrough=v);
+ } else {
+ bindVal('f-baseurl',v=>state.backend.base_url=v);
+ document.getElementById('add-header').onclick=()=>{ state.backend.headers.push(blankHeader()); save(); render(); };
+ document.querySelectorAll('#headers .prow').forEach(rowEl=>{ const i=+rowEl.dataset.hi;
+ rowEl.querySelector('.h-name').addEventListener('input',e=>{state.backend.headers[i].name=e.target.value;save();});
+ rowEl.querySelector('.h-value').addEventListener('input',e=>{state.backend.headers[i].value=e.target.value;save();});
+ rowEl.querySelector('.h-del').onclick=()=>{ state.backend.headers.splice(i,1); if(!state.backend.headers.length)state.backend.headers.push(blankHeader()); save(); render(); };
+ });
+ }
}
if (cur==='Methods'){
document.getElementById('add-method').onclick=()=>{ state.methods.push(blankMethod()); save(); render(); };
@@ -465,6 +519,9 @@ function wire() {
const on=(sel,fn)=>{ const el=cardEl.querySelector(sel); if(el) el.addEventListener('input',()=>{fn(el);save();schedulePreview();}); };
on('.m-name',el=>{m.name=el.value; cardEl.querySelector('.mname').textContent=el.value||ns()+'.method';});
on('.m-verb',el=>m.http.verb=el.value); on('.m-path',el=>m.http.path=el.value);
+ on('.m-args',el=>m.cli.args=el.value);
+ const mflags=cardEl.querySelector('.m-flags'); if(mflags) mflags.addEventListener('change',()=>{m.cli.params_as_flags=mflags.checked;save();schedulePreview();});
+ const mpass=cardEl.querySelector('.m-passthrough'); if(mpass) mpass.addEventListener('change',()=>{m.cli.passthrough=mpass.checked;save();render();schedulePreview();});
on('.m-latency',el=>m.latency=el.value); on('.m-description',el=>m.description=el.value);
const del=cardEl.querySelector('.m-del'); if(del) del.onclick=()=>{ state.methods.splice(i,1); if(!state.methods.length)state.methods.push(blankMethod()); save(); render(); };
cardEl.querySelector('.p-add').onclick=()=>{ m.params.push(blankParam()); save(); render(); };
@@ -505,12 +562,23 @@ function validateStep(){
if(!/^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/.test(state.version)) bad('e-version','Semver, e.g. 0.1.0'); else show('e-version','');
if(!state.description.trim()) bad('e-description','Required'); else show('e-description','');
}
- if (cur==='Backend'){ if(!/^https?:\/\/[^\s/]+/.test(state.backend.base_url)) bad('e-baseurl','Absolute http(s) URL'); else show('e-baseurl',''); }
+ if (cur==='Backend'){
+ if (state.app_type==='cli'){
+ if(!state.backend.command.trim()) bad('e-command','Enter the command to run, e.g. gh'); else show('e-command','');
+ } else {
+ if(!/^https?:\/\/[^\s/]+/.test(state.backend.base_url)) bad('e-baseurl','Absolute http(s) URL'); else show('e-baseurl','');
+ }
+ }
if (cur==='Methods'){
const ms=state.methods.filter(m=>m.name.trim());
if(!ms.length) bad('e-methods','Add at least one method');
- else { const badm=ms.find(m=>!m.latency||!m.description.trim()||!/^\//.test(m.http.path)||!m.name.startsWith(ns()+'.'));
- if(badm) bad('e-methods','Each method needs a '+ns()+'.-prefixed name, path starting with /, a latency, and a description'); else show('e-methods',''); }
+ else if (state.app_type==='cli'){
+ const badm=ms.find(m=>!m.latency||!m.description.trim()||!m.name.startsWith(ns()+'.')||(!m.cli.passthrough && !m.cli.args.trim() && !m.cli.params_as_flags));
+ if(badm) bad('e-methods','Each CLI method needs a '+ns()+'.-prefixed name, a latency, a description, and either arguments, params-as-flags, or passthrough'); else show('e-methods','');
+ } else {
+ const badm=ms.find(m=>!m.latency||!m.description.trim()||!/^\//.test(m.http.path)||!m.name.startsWith(ns()+'.'));
+ if(badm) bad('e-methods','Each method needs a '+ns()+'.-prefixed name, path starting with /, a latency, and a description'); else show('e-methods','');
+ }
}
return ok;
}
@@ -534,14 +602,18 @@ function renderReview(){
const s=submission();
// Each value carries already-escaped HTML; the only markup allowed in a value
// is the separators in Methods. All user-controlled text goes through esc().
+ const cli=state.app_type==='cli';
+ const routeStr=m=>cli ? (m.cli.passthrough?'passthrough':((m.cli.args||[]).join(' ')||'flags')+(m.cli.params_as_flags?' +flags':'')) : `${m.http.verb} ${m.http.path}`;
const rows=[
['App ID',esc(s.id)],['Version',esc(s.version)],['Description',esc(s.description)],
- ['Backend',esc(s.backend.base_url)],
- ...(s.backend.headers.map(h=>['Header',esc(h.name+': '+h.value)])),
- ['Methods',s.methods.map(m=>esc(`${m.name} (${m.http.verb} ${m.http.path}, ${m.latency})`)).join(' ')],
+ ['App type',cli?'CLI':'HTTP API'],
+ cli ? ['Command',esc((s.backend.command||[]).join(' '))] : ['Backend',esc(s.backend.base_url)],
+ ...(cli
+ ? ((s.backend.env_passthrough||[]).length ? [['Env passthrough',esc((s.backend.env_passthrough||[]).join(', '))]] : [])
+ : s.backend.headers.map(h=>['Header',esc(h.name+': '+h.value)])),
+ ['Methods',s.methods.map(m=>esc(`${m.name} (${routeStr(m)}, ${m.latency})`)).join(' ')],
['Display name',esc(s.listing.display_name)],['License',esc(s.listing.license)],
['App description',esc(s.listing.app_description)],['Categories',esc((s.listing.categories||[]).join(', '))],
- ['App type','HTTP API'],
['Vendor',esc(s.vendor.name+(s.vendor.url?' · '+s.vendor.url:''))],['Email',esc(s.email)],
['Agent usage',esc(s.vendor.agent_usage)],['Capabilities',esc(s.vendor.capabilities)],
['Signed by',esc(s.release.signer_name)],