@xtrn/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/cli.js +66 -0
- package/package.json +34 -0
- package/templates/README.md.template +289 -0
- package/templates/dev-vars.example.template +4 -0
- package/templates/index.ts.template +22 -0
- package/templates/package.json.template +20 -0
- package/templates/tsconfig.json.template +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @xtrn/cli
|
|
2
|
+
|
|
3
|
+
CLI toolkit for developing and submitting XTRN tool servers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add -g @xtrn/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use per-project:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun add -d @xtrn/cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `xtrn init <name> <version>`
|
|
20
|
+
|
|
21
|
+
Scaffold a new XTRN server project.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
xtrn init my-server v1.0.0
|
|
25
|
+
cd my-server
|
|
26
|
+
bun install
|
|
27
|
+
xtrn dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Creates a directory with:
|
|
31
|
+
- `index.ts` — Server entry point with example configuration
|
|
32
|
+
- `package.json` — Dependencies and scripts
|
|
33
|
+
- `tsconfig.json` — TypeScript configuration
|
|
34
|
+
- `.dev.vars.example` — OAuth environment variable template
|
|
35
|
+
- `README.md` — Full API guide
|
|
36
|
+
|
|
37
|
+
Arguments:
|
|
38
|
+
- `name` — Server name in slug format (e.g., `my-server`)
|
|
39
|
+
- `version` — Semver with v prefix (e.g., `v1.0.0`)
|
|
40
|
+
|
|
41
|
+
### `xtrn dev [entry] [--port=N]`
|
|
42
|
+
|
|
43
|
+
Start a local dev server using wrangler with Durable Objects.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
xtrn dev # ./index.ts on port 1234
|
|
47
|
+
xtrn dev ./server.ts # Custom entry point
|
|
48
|
+
xtrn dev --port=3000 # Custom port
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Automatically loads `.dev.vars` for OAuth secrets and other environment variables.
|
|
52
|
+
|
|
53
|
+
### `xtrn submit`
|
|
54
|
+
|
|
55
|
+
Submit your server to the [xtrn-servers](https://github.com/AbhinavPalacharla/xtrn-servers) registry.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
xtrn submit
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This forks the xtrn-servers repo and opens a PR with your server code. Requires [GitHub CLI](https://cli.github.com/) (`gh`).
|
|
62
|
+
|
|
63
|
+
## Getting Started
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
bun add -g @xtrn/cli
|
|
67
|
+
xtrn init my-server v1.0.0
|
|
68
|
+
cd my-server
|
|
69
|
+
bun install
|
|
70
|
+
xtrn dev
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then open `http://localhost:1234/details`.
|
|
74
|
+
|
|
75
|
+
See [@xtrn/server](https://www.npmjs.com/package/@xtrn/server) for the full server framework API.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{spawn as V}from"node:child_process";import*as R from"node:fs";import*as _ from"node:path";import{spawn as Y}from"node:child_process";import*as x from"node:fs";import*as M from"node:os";import*as g from"node:path";function X(o){let n=o;while(!0){let s=g.join(n,"node_modules");if(x.existsSync(s))return s;let t=g.dirname(n);if(t===n)return null;n=t}}function O(o,n,s){let t=g.dirname(o),e=X(t);if(!e)console.warn("[xtrn] Warning: node_modules not found for symlink");let i=g.join(s,"node_modules");if(e&&!x.existsSync(i))x.symlinkSync(e,i,"junction");let a=`export { XTRNState } from "@xtrn/server";
|
|
3
|
+
export { default } from "${o}";
|
|
4
|
+
`;x.writeFileSync(g.join(s,"_cf_entry.ts"),a);let c=`name = "xtrn-dev"
|
|
5
|
+
main = "_cf_entry.ts"
|
|
6
|
+
compatibility_date = "2026-02-14"
|
|
7
|
+
compatibility_flags = ["nodejs_compat"]
|
|
8
|
+
|
|
9
|
+
[vars]
|
|
10
|
+
${Object.entries(n).map(([m,l])=>`${m} = "${l}"`).join(`
|
|
11
|
+
`)}
|
|
12
|
+
|
|
13
|
+
[[durable_objects.bindings]]
|
|
14
|
+
name = "XTRN_STATE"
|
|
15
|
+
class_name = "XTRNState"
|
|
16
|
+
|
|
17
|
+
[[migrations]]
|
|
18
|
+
tag = "v1"
|
|
19
|
+
new_sqlite_classes = ["XTRNState"]
|
|
20
|
+
`;x.writeFileSync(g.join(s,"wrangler.toml"),c)}async function A(o,n){let s=g.dirname(o),t=g.basename(o),e=g.join(n,t);x.copyFileSync(o,e);let i=X(s);if(!i)console.warn("[xtrn] Warning: node_modules not found for symlink");let a=g.join(n,"node_modules");if(i&&!x.existsSync(a))x.symlinkSync(i,a,"junction");let c=`export { XTRNState } from "@xtrn/server";
|
|
21
|
+
export { default } from "${`./${t}`}";
|
|
22
|
+
`,m=g.join(n,"_cf_entry.ts");x.writeFileSync(m,c);let l=`name = "xtrn-dev"
|
|
23
|
+
main = "_cf_entry.ts"
|
|
24
|
+
compatibility_date = "2026-02-14"
|
|
25
|
+
compatibility_flags = ["nodejs_compat"]
|
|
26
|
+
|
|
27
|
+
[[durable_objects.bindings]]
|
|
28
|
+
name = "XTRN_STATE"
|
|
29
|
+
class_name = "XTRNState"
|
|
30
|
+
|
|
31
|
+
[[migrations]]
|
|
32
|
+
tag = "v1"
|
|
33
|
+
new_sqlite_classes = ["XTRNState"]
|
|
34
|
+
`;return x.writeFileSync(g.join(n,"wrangler.toml"),l),await Z(n),g.join(n,"_cf_entry.js")}function Z(o){return new Promise((n,s)=>{let t=Y("wrangler",["deploy","--dry-run","--outdir","."],{cwd:o,stdio:["ignore","pipe","pipe"]}),e="";t.stdout?.on("data",()=>{}),t.stderr?.on("data",(i)=>{e+=i.toString()}),t.on("error",(i)=>{s(Error(`Failed to spawn wrangler: ${i.message}`))}),t.on("close",(i)=>{if(i!==0){s(Error(`wrangler deploy --dry-run failed (exit ${i}):
|
|
35
|
+
${e}`));return}n()})})}function $(){return x.mkdtempSync(g.join(M.tmpdir(),"xtrn-dev-"))}function S(o){x.rmSync(o,{recursive:!0,force:!0})}import*as N from"node:fs";import*as F from"node:path";import{Miniflare as G}from"miniflare";function K(o,n,s,t){let e=t||F.dirname(o);return{modules:!0,scriptPath:o,modulesRoot:e,compatibilityDate:"2026-02-14",compatibilityFlags:["nodejs_compat"],durableObjects:{XTRN_STATE:"XTRNState"},durableObjectsPersist:!0,port:n,bindings:s}}async function C(o,n,s,t){let e=K(o,n,s,t),i=new G(e);return await i.ready,i}function q(o){if(!N.existsSync(o))return{};let n=N.readFileSync(o,"utf-8"),s={};for(let t of n.split(`
|
|
36
|
+
`)){let e=t.trim();if(!e||e.startsWith("#"))continue;let i=e.indexOf("=");if(i===-1)continue;s[e.slice(0,i)]=e.slice(i+1)}return s}async function W(o){return await(await o.dispatchFetch("http://localhost/details")).json()}var r={reset:"\x1B[0m",bold:"\x1B[1m",dim:"\x1B[2m",cyan:"\x1B[36m",green:"\x1B[32m",yellow:"\x1B[33m",magenta:"\x1B[35m",white:"\x1B[37m",red:"\x1B[31m",gray:"\x1B[90m"};function P(o){let n="./index.ts",s=1234;for(let t of o)if(t.startsWith("--port="))s=Number.parseInt(t.slice(t.indexOf("=")+1),10);else if(!t.startsWith("--"))n=t;return{entryPoint:_.resolve(n),port:s}}async function k(o){for(let s=0;s<5;s++){try{let t=await fetch(`http://localhost:${o}/details`);if(t.ok)return await t.json()}catch{}await new Promise((t)=>setTimeout(t,200))}throw Error("Failed to fetch /details after server ready")}function D(o,n){let s=`http://localhost:${o}`;if(console.log(""),console.log(` ${r.bold}${r.cyan}${n.name}${r.reset} ${r.dim}${n.version}${r.reset}`),console.log(` ${r.green}➜${r.reset} ${r.bold}${s}${r.reset}`),console.log(""),n.tools.length>0){console.log(` ${r.bold}${r.white}Tools${r.reset}`);for(let t of n.tools){let e=t.tags.length>0?` ${r.dim}${t.tags.map((i)=>`[${i}]`).join(" ")}${r.reset}`:"";console.log(` ${r.cyan}${t.name}${r.reset}${e} ${r.gray}— ${t.description}${r.reset}`)}console.log("")}if(n.config.length>0){console.log(` ${r.bold}${r.white}Config${r.reset}`);for(let t of n.config)console.log(` ${r.yellow}${t.key}${r.reset} ${r.dim}${t.type}${r.reset}`);console.log("")}if(n.oauth)console.log(` ${r.bold}${r.white}OAuth${r.reset} ${r.magenta}${n.oauth.provider}${r.reset}`),console.log(` ${r.dim}authorize${r.reset} ${n.oauth.authorization_url}`),console.log(` ${r.dim}token${r.reset} ${n.oauth.token_url}`),console.log(` ${r.dim}callback${r.reset} ${n.oauth.callback_url}`),console.log(` ${r.dim}scopes${r.reset} ${n.oauth.scopes.join(", ")}`),console.log("")}function nn(o,n){let s=_.join(o,"wrangler.toml"),t=V("npx",["wrangler","dev","--config",s,"--port",String(n)],{cwd:o,stdio:["ignore","pipe","pipe"]}),e="",i=new Promise((a,f)=>{let c=(l)=>{if(l.toString().includes("Ready on"))t.stdout?.off("data",c),a()},m=(l)=>{e+=l.toString()};t.stdout?.on("data",c),t.stderr?.on("data",m),t.on("error",(l)=>f(l)),t.on("close",(l)=>{if(l!==0&&l!==null)f(Error(e.trim()||`wrangler exited with code ${l}`))})});return i.then(()=>{e="",t.stderr?.on("data",(a)=>{let f=a.toString();if(f.includes("ERROR")||f.includes("Error")||f.includes("error:"))process.stderr.write(`${r.red}[xtrn dev]${r.reset} ${f}`)})}),{proc:t,ready:i}}async function I(o){let{entryPoint:n,port:s}=P(o),t=_.dirname(n);if(!R.existsSync(n))console.error(`${r.red}[xtrn dev]${r.reset} Entry point not found: ${n}`),process.exit(1);if(!R.existsSync(_.join(t,"package.json")))console.error(`${r.red}[xtrn dev]${r.reset} No package.json found in ${t}`),process.exit(1);let e=_.join(t,".dev.vars"),i=q(e),a=$();O(n,i,a);let f=null,c=()=>{if(console.log(`
|
|
37
|
+
${r.dim}[xtrn dev] Shutting down...${r.reset}`),f)f.kill(),f=null;S(a),process.exit(0)};process.on("SIGINT",c),process.on("SIGTERM",c);try{console.log(`${r.dim}[xtrn dev] Starting...${r.reset}`);let m=nn(a,s);f=m.proc,f.on("error",(u)=>{console.error(`${r.red}[xtrn dev]${r.reset} Failed to start: ${u.message}`),S(a),process.exit(1)}),f.on("close",(u)=>{if(u!==0&&u!==null)console.error(`${r.red}[xtrn dev]${r.reset} Process exited with code ${u}`);S(a),process.exit(u??1)}),await m.ready;let l=await k(s);D(s,l),await new Promise(()=>{})}catch(m){if(f)f.kill();if(S(a),m instanceof Error)console.error(`${r.red}[xtrn dev]${r.reset} ${m.message}`);process.exit(1)}}import{mkdir as U,readFile as tn,writeFile as sn}from"node:fs/promises";import{dirname as on,join as h}from"node:path";import{fileURLToPath as en}from"node:url";async function H(o){let n=o[0],s=o[1]??"v1.0.0";if(!n)console.error("Error: <name> argument is required"),console.error("Usage: xtrn new <name> [version]"),process.exit(1);let t=s.startsWith("v")?s:`v${s}`,e=t.replace(/^v/,"");if(!/^\d+\.\d+\.\d+$/.test(e))console.error(`Error: Invalid version "${s}". Expected semver (e.g., v1.0.0)`),process.exit(1);let i=en(import.meta.url),a=on(i),f=h(a,"../../templates"),c=h(process.cwd(),n);try{await U(c,{recursive:!1})}catch(m){if(m instanceof Error&&"code"in m&&m.code==="EEXIST")console.error(`Error: Directory "${n}" already exists`),process.exit(1);throw m}try{let m=[{src:"index.ts.template",dst:"index.ts"},{src:"package.json.template",dst:"package.json"},{src:"tsconfig.json.template",dst:"tsconfig.json"}];for(let l of m){let u=h(f,l.src),j=h(c,l.dst),w=await tn(u,"utf-8");if(w=w.replace(/\{\{NAME\}\}/g,n),w=w.replace(/\{\{VERSION\}\}/g,t),w=w.replace(/\{\{VERSION_BARE\}\}/g,e),l.dst==="package.json"){let d=JSON.parse(w);d.dependencies["@xtrn/server"]="^1.0.0",d.devDependencies.xtrn="^1.0.0",d.scripts.submit="xtrn submit",w=`${JSON.stringify(d,null,"\t")}
|
|
38
|
+
`}await sn(j,w,"utf-8")}console.log(`✓ Created ${n}/`),console.log(""),console.log("Next steps:"),console.log(` cd ${n}`),console.log(" bun install"),console.log(" bun run dev")}catch(m){try{await U(c,{recursive:!0})}catch{}throw m}}import{spawn as rn}from"node:child_process";import*as y from"node:fs";import{cp as J,mkdir as z,readFile as an,readdir as fn,writeFile as mn}from"node:fs/promises";import*as T from"node:os";import*as v from"node:path";var p="xtrnai/servers",ln=new Set(["node_modules","bun.lock",".dev.vars","dist","_cf_entry.ts","wrangler.toml",".wrangler",".git",".github"]);function b(o,n,s){return new Promise((t)=>{let e=rn(o,n,{stdio:"pipe",cwd:s?.cwd}),i="",a="";e.stdout.on("data",(f)=>{i+=f.toString()}),e.stderr.on("data",(f)=>{a+=f.toString()}),e.on("close",(f)=>{t({stdout:i.trim(),stderr:a.trim(),code:f??1})}),e.on("error",()=>{t({stdout:"",stderr:`Failed to execute: ${o}`,code:1})})})}async function xn(){if((await b("gh",["--version"])).code!==0)console.error("[xtrn submit] GitHub CLI (gh) is not installed."),console.error(""),console.error("Install it:"),console.error(" macOS: brew install gh"),console.error(" Linux: https://github.com/cli/cli/blob/trunk/docs/install_linux.md"),console.error(" Windows: winget install --id GitHub.cli"),console.error(""),console.error("Then authenticate:"),console.error(" gh auth login"),process.exit(1);if((await b("gh",["auth","status"])).code!==0)console.error("[xtrn submit] GitHub CLI is not authenticated."),console.error(""),console.error("Run:"),console.error(" gh auth login"),process.exit(1)}function gn(o){if(!y.existsSync(v.join(o,"index.ts")))console.error("[xtrn submit] No index.ts found in current directory."),console.error("Run this command from your XTRN server project root."),process.exit(1);if(!y.existsSync(v.join(o,"package.json")))console.error("[xtrn submit] No package.json found in current directory."),console.error("Run this command from your XTRN server project root."),process.exit(1)}async function cn(o){let n=v.resolve(o,"index.ts"),s=$(),t=null;try{console.log("[xtrn submit] Bundling server...");let e=await A(n,s);console.log("[xtrn submit] Starting headless Miniflare..."),t=await C(e,0,{});let i=await W(t),a=i.name,f=i.version;if(typeof a!=="string"||!a)throw Error("Server /details missing 'name' field");if(typeof f!=="string"||!f)throw Error("Server /details missing 'version' field");return console.log(`[xtrn submit] Detected: ${a} v${f}`),{name:a,version:f}}finally{if(t)await t.dispose();S(s)}}async function B(o,n){await z(n,{recursive:!0});let s=await fn(o,{withFileTypes:!0});for(let t of s){if(ln.has(t.name))continue;let e=v.join(o,t.name),i=v.join(n,t.name);if(t.isDirectory())await B(e,i);else await J(e,i)}}function un(o){let n=JSON.parse(o),s=(t)=>{if(!t||typeof t!=="object")return;let e={};for(let[i,a]of Object.entries(t))if(typeof a==="string"&&(a.startsWith("file:")||a.startsWith("link:")||a.startsWith("workspace:")))e[i]="^1.0.0";else e[i]=a;return e};if(n.dependencies)n.dependencies=s(n.dependencies);if(n.devDependencies)n.devDependencies=s(n.devDependencies);return`${JSON.stringify(n,null,"\t")}
|
|
39
|
+
`}async function vn(o,n){console.log("[xtrn submit] Preparing files..."),await B(o,n);let s=v.join(n,"package.json");if(y.existsSync(s)){let t=await an(s,"utf-8"),e=un(t);await mn(s,e,"utf-8"),console.log("[xtrn submit] Rewrote package.json (local deps → ^1.0.0)")}}async function yn(o,n,s){let t=y.mkdtempSync(v.join(T.tmpdir(),"xtrn-submit-")),e=v.join(t,"xtrn-servers");try{console.log(`[xtrn submit] Forking ${p}...`);let i=await b("gh",["repo","fork",p,"--clone=true","--default-branch-only"],{cwd:t});if(i.code!==0&&!i.stderr.includes("already exists"))throw Error(`Fork failed: ${i.stderr}`);if(!y.existsSync(e))throw Error(`Expected cloned repo at ${e}, not found. Output: ${i.stdout} ${i.stderr}`);let a=`submit/${o}/v${n}`;console.log(`[xtrn submit] Creating branch ${a}...`);let f=await b("git",["checkout","-b",a],{cwd:e});if(f.code!==0)throw Error(`Branch creation failed: ${f.stderr}`);let c=v.join(e,"servers",o,`v${n}`);await z(c,{recursive:!0}),await J(s,c,{recursive:!0}),console.log("[xtrn submit] Committing...");let m=await b("git",["add","."],{cwd:e});if(m.code!==0)throw Error(`git add failed: ${m.stderr}`);let l=await b("git",["commit","-m",`Add ${o} v${n}`],{cwd:e});if(l.code!==0)throw Error(`git commit failed: ${l.stderr}`);console.log("[xtrn submit] Pushing...");let u=await b("git",["push","origin",a],{cwd:e});if(u.code!==0)throw Error(`git push failed: ${u.stderr}`);console.log("[xtrn submit] Creating pull request...");let j=await b("gh",["pr","create","--repo",p,"--title",`Add ${o} v${n}`,"--body",`Automated submission via \`xtrn submit\` CLI.
|
|
40
|
+
|
|
41
|
+
**Server:** ${o}
|
|
42
|
+
**Version:** v${n}`],{cwd:e});if(j.code!==0)throw Error(`PR creation failed: ${j.stderr}`);return j.stdout}finally{y.rmSync(t,{recursive:!0,force:!0})}}async function L(o){let n=process.cwd();await xn(),gn(n);let{name:s,version:t}=await cn(n),e=y.mkdtempSync(v.join(T.tmpdir(),"xtrn-prepared-"));try{await vn(n,e);let i=await yn(s,t,e);console.log(""),console.log("✓ Pull request created!"),console.log(` ${i}`),console.log(""),console.log("Your server will be reviewed and deployed automatically once merged.")}finally{y.rmSync(e,{recursive:!0,force:!0})}}var Q="1.0.0",E=`
|
|
43
|
+
xtrn v${Q} — XTRN server development toolkit
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
xtrn new <name> <version> Scaffold a new XTRN server project
|
|
47
|
+
xtrn dev [entry] [--port=N] Start dev server with wrangler
|
|
48
|
+
xtrn submit Submit server to xtrn-servers registry
|
|
49
|
+
xtrn --help Show this help
|
|
50
|
+
xtrn --version Show version
|
|
51
|
+
|
|
52
|
+
New arguments:
|
|
53
|
+
name Server name in slug format (e.g., my-server)
|
|
54
|
+
version Semver with v prefix (e.g., v1.0.0)
|
|
55
|
+
|
|
56
|
+
Dev options:
|
|
57
|
+
entry Entry point file (default: ./index.ts)
|
|
58
|
+
--port=N Port to run on (default: 1234)
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
xtrn new my-server v1.0.0 Create a new server project
|
|
62
|
+
xtrn dev Start dev server with ./index.ts on port 1234
|
|
63
|
+
xtrn dev ./server.ts Start dev server with custom entry
|
|
64
|
+
xtrn dev --port=3000 Start dev server on port 3000
|
|
65
|
+
xtrn submit Fork xtrn-servers and open PR
|
|
66
|
+
`.trim();async function wn(){let o=process.argv.slice(2),n=o[0];if(!n||n==="--help"||n==="-h")console.log(E),process.exit(0);if(n==="--version"||n==="-v")console.log(`xtrn v${Q}`),process.exit(0);if(n==="new"){let s=o.slice(1);if(s.includes("--help")||s.includes("-h"))console.log(E),process.exit(0);await H(s);return}if(n==="dev"){let s=o.slice(1);if(s.includes("--help")||s.includes("-h"))console.log(E),process.exit(0);await I(s);return}if(n==="submit"){let s=o.slice(1);if(s.includes("--help")||s.includes("-h"))console.log(E),process.exit(0);await L(s);return}console.error(`Unknown command: ${n}`),console.error('Run "xtrn --help" for usage.'),process.exit(1)}wn().catch((o)=>{console.error("[xtrn] Fatal:",o),process.exit(1)});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtrn/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"xtrn": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"templates"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "bun run build:clean && bun run build:bun && bun run build:types",
|
|
17
|
+
"build:clean": "rm -rf dist",
|
|
18
|
+
"build:bun": "bun build src/cli.ts --outdir dist --target node --format esm --minify --external miniflare",
|
|
19
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
20
|
+
"build:watch": "bun run build:types --watch",
|
|
21
|
+
"prepublishOnly": "bun run build",
|
|
22
|
+
"test": "bun test"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@biomejs/biome": "2.3.4",
|
|
26
|
+
"@types/bun": "latest",
|
|
27
|
+
"@xtrn/server": "workspace:*",
|
|
28
|
+
"typescript": "^5.0.0",
|
|
29
|
+
"zod": "^4.3.6"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"miniflare": "^3.20250115.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# {{NAME}}
|
|
2
|
+
|
|
3
|
+
An XTRN server. Built with [@xtrn/server](https://www.npmjs.com/package/@xtrn/server) on Hono + Cloudflare Workers with Zod v4 validation, OAuth 2.0 integration, and lifecycle management via Durable Objects.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
bun install
|
|
10
|
+
|
|
11
|
+
# Start dev server (port 1234)
|
|
12
|
+
xtrn dev
|
|
13
|
+
|
|
14
|
+
# Custom port
|
|
15
|
+
xtrn dev --port=3000
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then open `http://localhost:1234/details`.
|
|
19
|
+
|
|
20
|
+
## Server API
|
|
21
|
+
|
|
22
|
+
Import everything from `@xtrn/server/server`:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { XTRNServer, defineConfig, ToolTag } from "@xtrn/server/server";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### OPEN Server
|
|
30
|
+
|
|
31
|
+
No configuration or authentication required.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { XTRNServer, defineConfig } from "@xtrn/server/server";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
const server = new XTRNServer({
|
|
38
|
+
name: "hello-world",
|
|
39
|
+
version: "1.0.0",
|
|
40
|
+
config: defineConfig({}),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
server.registerTool({
|
|
44
|
+
name: "greet",
|
|
45
|
+
description: "Returns a friendly greeting",
|
|
46
|
+
schema: z.object({
|
|
47
|
+
name: z.string().describe("Name of the person to greet"),
|
|
48
|
+
}),
|
|
49
|
+
handler: (ctx) => {
|
|
50
|
+
return ctx.res.text(`Hello, ${ctx.req.name}!`);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export default server;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Config-Only Server
|
|
58
|
+
|
|
59
|
+
Requires users to provide configuration values (API keys, preferences).
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { XTRNServer, defineConfig } from "@xtrn/server/server";
|
|
63
|
+
import { z } from "zod";
|
|
64
|
+
|
|
65
|
+
const server = new XTRNServer({
|
|
66
|
+
name: "config-server",
|
|
67
|
+
version: "1.0.0",
|
|
68
|
+
config: defineConfig({
|
|
69
|
+
userConfig: [
|
|
70
|
+
{ key: "apiKey", type: "string" },
|
|
71
|
+
{ key: "maxRetries", type: "number" },
|
|
72
|
+
{ key: "debugMode", type: "boolean" },
|
|
73
|
+
],
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
server.registerTool({
|
|
78
|
+
name: "fetch-data",
|
|
79
|
+
description: "Fetch data using API key",
|
|
80
|
+
schema: z.object({ id: z.string() }),
|
|
81
|
+
handler: (ctx) => {
|
|
82
|
+
// ctx.config.apiKey — string
|
|
83
|
+
// ctx.config.maxRetries — number
|
|
84
|
+
// ctx.config.debugMode — boolean
|
|
85
|
+
return ctx.res.json({ id: ctx.req.id, key: ctx.config.apiKey });
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export default server;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### OAuth-Only Server
|
|
93
|
+
|
|
94
|
+
Requires users to complete an OAuth flow. The platform provides a fresh access token directly.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { XTRNServer, defineConfig } from "@xtrn/server/server";
|
|
98
|
+
import { z } from "zod";
|
|
99
|
+
|
|
100
|
+
const server = new XTRNServer({
|
|
101
|
+
name: "github-server",
|
|
102
|
+
version: "1.0.0",
|
|
103
|
+
config: defineConfig({
|
|
104
|
+
oauthConfig: {
|
|
105
|
+
provider: "github",
|
|
106
|
+
authorization_url: "https://github.com/login/oauth/authorize",
|
|
107
|
+
token_url: "https://github.com/login/oauth/access_token",
|
|
108
|
+
scopes: ["repo", "user"],
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
server.registerTool({
|
|
114
|
+
name: "get-repos",
|
|
115
|
+
description: "List user repositories",
|
|
116
|
+
schema: z.object({
|
|
117
|
+
sort: z.enum(["created", "updated", "pushed"]).default("updated"),
|
|
118
|
+
}),
|
|
119
|
+
handler: async (ctx) => {
|
|
120
|
+
// ctx.accessToken — ready-to-use access token string
|
|
121
|
+
const resp = await fetch("https://api.github.com/user/repos?sort=" + ctx.req.sort, {
|
|
122
|
+
headers: { Authorization: `Bearer ${ctx.accessToken}` },
|
|
123
|
+
});
|
|
124
|
+
return ctx.res.json(await resp.json());
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export default server;
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Config + OAuth Server
|
|
132
|
+
|
|
133
|
+
Combines both user configuration and OAuth authentication.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { XTRNServer, defineConfig, ToolTag } from "@xtrn/server/server";
|
|
137
|
+
import { z } from "zod";
|
|
138
|
+
|
|
139
|
+
const server = new XTRNServer({
|
|
140
|
+
name: "calendar-server",
|
|
141
|
+
version: "1.0.0",
|
|
142
|
+
config: defineConfig({
|
|
143
|
+
userConfig: [
|
|
144
|
+
{ key: "timezone", type: "string" },
|
|
145
|
+
],
|
|
146
|
+
oauthConfig: {
|
|
147
|
+
provider: "google-calendar",
|
|
148
|
+
authorization_url: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
149
|
+
token_url: "https://oauth2.googleapis.com/token",
|
|
150
|
+
scopes: ["https://www.googleapis.com/auth/calendar"],
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
server.registerTool({
|
|
156
|
+
name: "list-events",
|
|
157
|
+
description: "List calendar events",
|
|
158
|
+
tags: [ToolTag.Mutation],
|
|
159
|
+
schema: z.object({
|
|
160
|
+
maxResults: z.number().default(10),
|
|
161
|
+
}),
|
|
162
|
+
handler: async (ctx) => {
|
|
163
|
+
// ctx.config.timezone — string (from X-XTRN-Config header)
|
|
164
|
+
// ctx.accessToken — string (from X-XTRN-Access-Token header)
|
|
165
|
+
const resp = await fetch(
|
|
166
|
+
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
|
|
167
|
+
{ headers: { Authorization: `Bearer ${ctx.accessToken}` } },
|
|
168
|
+
);
|
|
169
|
+
return ctx.res.json(await resp.json());
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export default server;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## OAuth Configuration
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
type OAuthConfig = {
|
|
180
|
+
provider: string;
|
|
181
|
+
authorization_url: string;
|
|
182
|
+
token_url: string;
|
|
183
|
+
scopes: string[];
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
OAuth secrets are managed via environment variables (not in code):
|
|
188
|
+
|
|
189
|
+
| Variable | Description |
|
|
190
|
+
|----------|-------------|
|
|
191
|
+
| `OAUTH_CLIENT_ID` | OAuth application client ID |
|
|
192
|
+
| `OAUTH_CLIENT_SECRET` | OAuth application client secret |
|
|
193
|
+
| `OAUTH_CALLBACK_URL` | OAuth redirect/callback URL |
|
|
194
|
+
|
|
195
|
+
Set these in a `.dev.vars` file for local development:
|
|
196
|
+
|
|
197
|
+
```env
|
|
198
|
+
OAUTH_CLIENT_ID=your-client-id
|
|
199
|
+
OAUTH_CLIENT_SECRET=your-client-secret
|
|
200
|
+
OAUTH_CALLBACK_URL=http://localhost:1234/auth/callback
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Context Object
|
|
204
|
+
|
|
205
|
+
| Property | Type | Availability | Description |
|
|
206
|
+
|----------|------|--------------|-------------|
|
|
207
|
+
| `ctx.req` | `T` (inferred from schema) | Always | Validated tool parameters |
|
|
208
|
+
| `ctx.config` | `Object` (inferred from userConfig) | Always (`{}` if no userConfig) | Validated user configuration |
|
|
209
|
+
| `ctx.accessToken` | `string` | OAuth servers only | Access token from platform |
|
|
210
|
+
| `ctx.res` | `XTRNResponse` | Always | Response helper methods |
|
|
211
|
+
|
|
212
|
+
## Tool Tags
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { ToolTag } from "@xtrn/server/server";
|
|
216
|
+
|
|
217
|
+
server.registerTool({
|
|
218
|
+
name: "delete-item",
|
|
219
|
+
description: "Permanently delete an item",
|
|
220
|
+
tags: [ToolTag.Mutation, ToolTag.Destructive],
|
|
221
|
+
schema: z.object({ id: z.string() }),
|
|
222
|
+
handler: (ctx) => {
|
|
223
|
+
return ctx.res.json({ deleted: true });
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
| Tag | Meaning |
|
|
229
|
+
|-----|---------|
|
|
230
|
+
| `ToolTag.Mutation` | Tool modifies data |
|
|
231
|
+
| `ToolTag.Destructive` | Tool performs an irreversible action |
|
|
232
|
+
|
|
233
|
+
## Response Helpers
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
ctx.res.json({ data: "value" }) // 200 JSON
|
|
237
|
+
ctx.res.text("plain text") // 200 text
|
|
238
|
+
ctx.res.badRequestArgs("invalid x") // 400 Bad Request
|
|
239
|
+
ctx.res.unauthorized() // 401 Unauthorized
|
|
240
|
+
ctx.res.error("something failed") // 500 Internal Server Error
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Built-in Routes
|
|
244
|
+
|
|
245
|
+
| Route | Method | Description |
|
|
246
|
+
|-------|--------|-------------|
|
|
247
|
+
| `/details` | GET | Server metadata, tool definitions, JSON schemas |
|
|
248
|
+
| `/tools/{name}` | POST | Execute a tool |
|
|
249
|
+
| `/wind-down` | POST | Stop accepting new requests (graceful shutdown) |
|
|
250
|
+
| `/active-requests` | GET | Current in-flight request count and state |
|
|
251
|
+
| `/reset` | POST | Reset server state (dev only) |
|
|
252
|
+
|
|
253
|
+
## Calling Tools
|
|
254
|
+
|
|
255
|
+
Tools are called via POST to `/tools/{name}`. Configuration and tokens go in headers; tool parameters go in the body.
|
|
256
|
+
|
|
257
|
+
| Header | Format | When Required |
|
|
258
|
+
|--------|--------|---------------|
|
|
259
|
+
| `X-XTRN-Config` | `base64(JSON.stringify({...}))` | Server has `userConfig` |
|
|
260
|
+
| `X-XTRN-Access-Token` | `base64(access_token)` | Server has `oauthConfig` |
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
# OPEN server
|
|
264
|
+
curl -X POST http://localhost:1234/tools/greet \
|
|
265
|
+
-H "Content-Type: application/json" \
|
|
266
|
+
-d '{"name": "Alice"}'
|
|
267
|
+
|
|
268
|
+
# With user config
|
|
269
|
+
curl -X POST http://localhost:1234/tools/fetch-data \
|
|
270
|
+
-H "Content-Type: application/json" \
|
|
271
|
+
-H "X-XTRN-Config: $(echo '{"apiKey":"secret-123"}' | base64)" \
|
|
272
|
+
-d '{"id": "abc-123"}'
|
|
273
|
+
|
|
274
|
+
# With OAuth
|
|
275
|
+
curl -X POST http://localhost:1234/tools/get-repos \
|
|
276
|
+
-H "Content-Type: application/json" \
|
|
277
|
+
-H "X-XTRN-Access-Token: $(echo 'gho_xxxxxxxxxxxx' | base64)" \
|
|
278
|
+
-d '{"sort": "updated"}'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Submitting Your Server
|
|
282
|
+
|
|
283
|
+
When your server is ready, submit it to the registry:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
xtrn submit
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
This forks the [xtrn-servers](https://github.com/AbhinavPalacharla/xtrn-servers) repo and opens a PR with your server code. Requires [GitHub CLI](https://cli.github.com/) (`gh`).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { XTRNServer, defineConfig } from "@xtrn/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const server = new XTRNServer({
|
|
5
|
+
name: "{{NAME}}",
|
|
6
|
+
version: "{{VERSION}}",
|
|
7
|
+
config: defineConfig({}),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// Register your tools here
|
|
11
|
+
// server.registerTool({
|
|
12
|
+
// name: "example",
|
|
13
|
+
// description: "An example tool",
|
|
14
|
+
// schema: z.object({
|
|
15
|
+
// input: z.string().describe("Example input"),
|
|
16
|
+
// }),
|
|
17
|
+
// handler: (ctx) => {
|
|
18
|
+
// return ctx.res.json({ result: ctx.req.input });
|
|
19
|
+
// },
|
|
20
|
+
// });
|
|
21
|
+
|
|
22
|
+
export default server;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{NAME}}",
|
|
3
|
+
"version": "{{VERSION_BARE}}",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "xtrn dev"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@xtrn/server": "^0.1.0",
|
|
12
|
+
"zod": "^4.0.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "latest"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "^5"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUncheckedIndexedAccess": true,
|
|
17
|
+
"noImplicitOverride": true,
|
|
18
|
+
"noUnusedLocals": false,
|
|
19
|
+
"noUnusedParameters": false,
|
|
20
|
+
"noPropertyAccessFromIndexSignature": false
|
|
21
|
+
}
|
|
22
|
+
}
|