ai-ax 0.1.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 ADDED
@@ -0,0 +1,221 @@
1
+ # aiax
2
+
3
+ AI Agent Experience (AI/AX) library for Hono + Cloudflare.
4
+
5
+ 人間のための UI/UX と、LLM のための AI/AX を、1 つの操作面として実装するための library。
6
+ AI を特別扱いして専用画面を作るのではなく、AI を読者の一人として扱う。
7
+ 画面を見る人間と、木構造を観測する LLM は、同じ状態を読み、同じ操作 (act) で書く。
8
+ aiax はそのために必要な server 側の接続部 — 認証、認可、realtime 同期、MCP、LLM 中継 — を Hono middleware として提供する。
9
+
10
+ ## 0. 思想
11
+
12
+ web は人間が操作する前提で UI/UX を積み上げてきた。
13
+ そこへ LLM の操作経路を後付けすると、認証認可と操作面が人間用と AI 用の二重になり、どちらかが必ず壊れる。
14
+ aiax は操作の最小単位 action を 1 度だけ宣言させ、UI の click と MCP tool 呼び出しを同じ action に合流させる。
15
+ 状態は yjs の単一の木であり、人間には画面として、LLM には get_tree の観測結果として、同じ木が見える。
16
+
17
+ ```mermaid
18
+ flowchart TB
19
+ UI[UI/UX<br>人間の click] -->|同じ act| ACT[action 宣言<br>1 機能 1 file]
20
+ AX[AI/AX<br>LLM の tool 呼び出し] -->|同じ act| ACT
21
+ ACT <-->|読み書き| TREE[yjs Y.Map 'tree'<br>全状態 = 単一の木]
22
+ TREE -->|画面として| UI
23
+ TREE -->|get_tree として| AX
24
+ ```
25
+
26
+ ## 1. 全体構成
27
+
28
+ 前提は Hono + Cloudflare Workers。realtime 同期は partyserver (Durable Objects)、認証は Auth.js、LLM は Claude API の MCP connector。
29
+
30
+ ```mermaid
31
+ flowchart TB
32
+ B[Browser<br>React UI] -->|session<br>cookie| W[Hono Worker<br>aiax middleware]
33
+ C[Claude API] -->|grant<br>jwt| W
34
+ W -->|/api/llm| C
35
+ B <-->|websocket<br />sync| DO[Durable Object<br>y-partyserver]
36
+ W --> D1[(D1<br>Auth.js tables)]
37
+ W -->|認可済み room 名<br>x-user-sub header| DO
38
+ ```
39
+
40
+ worker には 4 つの middleware を並べるだけでよい。
41
+
42
+ ```ts
43
+ const ax = createAiax({ name: 'demo', own: ownSelf(), database: (e) => e.my_d1_demo, system: PROMPT })
44
+ const app = new Hono().use('*', ax.auth()).use('/parties/*', ax.party()).all('/api/mcp', ax.mcp()).post('/api/llm', ax.llm())
45
+ ```
46
+
47
+ ## 2. 認証と認可
48
+
49
+ credential は 2 種類だけ。どちらも AUTH_SECRET で署名された jwt であり、平文の user id が network を渡ることはない。
50
+
51
+ | 経路 | credential | 発行 | 検証 |
52
+ | -------------------------- | ------------------------------ | --------------------------------- | ------------------------------- |
53
+ | Browser → Worker | session cookie (Auth.js jwt) | /api/auth (Google OAuth) | userSub() |
54
+ | Claude → Worker (/api/mcp) | grant jwt ({ sub, path }, 12h) | llm middleware が認可後に mint | readGrant() |
55
+ | Worker → Durable Object | DO 名 + x-user-sub header | worker が認可済みの値で必ず上書き | DO へは worker binding 経由のみ |
56
+
57
+ 認可は own resolver という 1 つの関数に集約される。
58
+
59
+ ```ts
60
+ type Own = (env: any, sub: string, path: string) => Promise<string> | string
61
+ ```
62
+
63
+ 「誰 (sub) が、何 (path) に対して、どの部屋 (room) を得るか」を返す。空文字は拒否。
64
+ 所有の定義は service ごとに異なるため、resolver の実装は自由に差し替えられる。
65
+
66
+ ```ts
67
+ ownSite((env) => env.my_r2_xxx)
68
+ // R2 head(`${sub}/${path}`) の customMetadata.site が room。file を所有していれば editor の部屋が開く
69
+
70
+ ownSelf()
71
+ // room = sub。認証した本人の部屋だけが開く。start page や個人 app 向け
72
+
73
+ const ownGroup: Own = async (env, sub, path) => {
74
+ const [row] = await drizzle(env.my_d1).select().from(members).where(...)
75
+ return row ? row.roomId : ''
76
+ }
77
+ // D1 の所属 table を引く resolver も同じ型で書ける
78
+ ```
79
+
80
+ ## 3. 認可の流れ
81
+
82
+ ```mermaid
83
+ sequenceDiagram
84
+ autonumber
85
+ participant Browser
86
+ participant Worker
87
+ participant Resolver as own resolver
88
+ participant ClaudeAPI
89
+ participant MCP as Worker /api/mcp
90
+ participant DO as Durable Object
91
+
92
+ Browser->>Worker: POST /api/llm { path, prompt } (session cookie)
93
+ Worker->>Resolver: own(env, sub, path)
94
+ alt room = '' (所有なし)
95
+ Worker-->>Browser: 401 / 403
96
+ else room あり
97
+ Worker->>ClaudeAPI: messages + mcp_servers<br>(authorization_token = grant jwt { sub, path })
98
+ ClaudeAPI->>MCP: MCP request (Authorization: Bearer grant)
99
+ MCP->>MCP: readGrant (復号失敗 / 期限切れ → 401)
100
+ MCP->>Resolver: own(env, sub, path)
101
+ alt room = ''
102
+ MCP-->>ClaudeAPI: 403
103
+ else room あり
104
+ MCP->>DO: getServerByName(binding, room) へ action 転送
105
+ DO-->>Browser: 実行結果を websocket で realtime 同期
106
+ DO-->>MCP: action の戻り値
107
+ MCP-->>ClaudeAPI: MCP response
108
+ ClaudeAPI-->>Worker: stream 応答
109
+ Worker-->>Browser: text stream 中継
110
+ end
111
+ end
112
+ ```
113
+
114
+ grant jwt は llm middleware が turn ごとに mint し、Claude API がそのまま /api/mcp へ運ぶ。
115
+ LLM は token の中身を選べず、書き換えれば署名が壊れる。
116
+ つまり LLM が操作できる部屋は「その瞬間に認可された 1 部屋」だけに封じられる。
117
+
118
+ ## 4. room の決定規則
119
+
120
+ room 名は client にも LLM にも決めさせない。必ず own resolver の結果を使う。
121
+
122
+ - browser は 'my-room' という alias で接続してよい。party middleware が own の結果へ書き換えて DO へ渡す
123
+ - room 名を URL に持つ設計 (editor 型) では、URL の room と own の結果の一致を検査し、不一致は 403
124
+ - websocket・MCP どちらの経路でも x-user-sub / x-user-path header は worker が認可済みの値で上書きするため、外部から偽装できない
125
+ - lobby は唯一の例外。匿名で触れる demo 部屋を 1 つだけ開けられる。設定しなければ全経路で認証必須
126
+
127
+ ## 5. action
128
+
129
+ action は「UI handler が呼ぶ関数」と「MCP tool」の両方になる操作の最小単位。
130
+
131
+ ```ts
132
+ export const placeMark = action<{ cell: number }, string>(() => import('./actions/place-mark'), 'both', {
133
+ name: 'place_mark',
134
+ description: '空いているマスに現在の手番の印を置く',
135
+ input: { cell: z.number().describe('マス番号 0-8') },
136
+ })
137
+ ```
138
+
139
+ - 宣言から MCP tool (name / description / zod inputSchema) が自動生成される
140
+ - 第 1 引数は `() => import('./actions/xxx')` の dynamic import でも、実装関数そのものでもよい (bundle を分けたい service は前者、1 file で書きたい app は後者)
141
+ - side は 3 値。both = yjs を書くだけ (どちら側でも実行可)、client = browser API が要る (server から呼ばれると yjs 上の act-req / act-res で接続中 browser へ委譲)、server = DO 専用
142
+ - 人間の click も LLM の tool 呼び出しも同じ action 関数に到達するため、実装した全機能を LLM が操作・観測できる
143
+
144
+ ## 6. LLM 中継
145
+
146
+ /api/llm は「認可 → grant mint → Claude API 呼び出し → text stream 中継」を行う。
147
+
148
+ - mcp_servers に自 host の /api/mcp と grant jwt を設定して Claude API を呼ぶ
149
+ - stop_reason が pause_turn の間は messages を積み直して継続する (MCP tool 呼び出しを跨ぐ)
150
+ - tool 呼び出しは `[tool] name`、終端に `[usage] input/output tokens` を stream へ流す
151
+
152
+ ## 7. API
153
+
154
+ ### createAiax(config)
155
+
156
+ | key | 型 | 既定値 | 説明 |
157
+ | --------- | ------------------- | --------------------------------------- | -------------------------------------------------------------- |
158
+ | name | string | 必須 | service id。MCP server 名になる |
159
+ | own | Own | 必須 | 認可 resolver。(env, sub, path) → room。'' で拒否 |
160
+ | database | (env) => D1Database | なし | Auth.js の DrizzleAdapter に渡す D1。省略時は adapter なし jwt |
161
+ | lobby | string | なし | 匿名で開放する demo room。省略時は全経路で認証必須 |
162
+ | alias | string | 'my-room' | 「自分の部屋」を指す接続 room 名。own の結果へ書き換えられる |
163
+ | system | string | '' | LLM の system prompt |
164
+ | model | string | 'claude-sonnet-5' | Claude API の model id |
165
+ | claude | string | 'https://api.anthropic.com/v1/messages' | Claude API の URL (AI Gateway 経由に差し替え可) |
166
+ | maxTokens | number | 32000 | 1 応答の max_tokens |
167
+ | maxTurns | number | 10 | pause_turn 継続の上限回数 |
168
+ | binding | string | 'v1' | Durable Object binding 名 |
169
+ | endpoint | string | '/api/mcp' | MCP endpoint の path |
170
+ | version | string | '1.0.0' | MCP server の version 表記 |
171
+ | salt | string | 'authjs.mcp-token' | grant jwt の salt (service ごとの分離) |
172
+ | maxAge | number | 43200 (12h) | grant jwt の有効秒数 |
173
+ | cookie | { apex, services } | なし | subdomain 間で session cookie を共有する service 群 |
174
+ | providers | any[] | Google | Auth.js providers の差し替え |
175
+
176
+ 戻り値は `{ auth, party, mcp, llm }` の 4 middleware factory。
177
+ x-user-sub / x-user-path の header 名と anthropic-version は library 内部の protocol 定数であり、config では変えられない (両端が同時に変わる必要があるため)。
178
+
179
+ ### 個別 export
180
+
181
+ | export | file | 説明 |
182
+ | ---------------------------------------------------------- | --------- | ------------------------------------------------------------------ |
183
+ | action / actions / init / serve / grantOf / side | act.ts | action system。宣言と登録簿、act-req/act-res の配線、DO 側の受け口 |
184
+ | auth / userSub / signGrant / readGrant / ownSite / ownSelf | auth.ts | 認証と認可の素材。Auth.js 設定、grant jwt、own preset |
185
+ | party | party.ts | websocket 経路の認可と room 書き換え・DO 転送 |
186
+ | mcp | mcp.ts | grant 検証・認可・action の MCP tool 化・DO への action 転送 |
187
+ | llm | llm.ts | Claude API 中継 stream (認可 → mint → converse) |
188
+ | callClaude / relay / converse | claude.ts | Claude API 呼び出しと stream の逐次処理・turn 継続 |
189
+
190
+ ## 8. 適用パターン
191
+
192
+ [Hono and React realtime app](https://zenn.dev/jp/articles) の template がそのまま前提環境になる。
193
+ 記事の authMiddleware / myMiddleware を ax.auth() / ax.party() に差し替え、/api/mcp と /api/llm の 2 行を足すと、realtime app が AI/AX 対応になる。
194
+
195
+ | 型 | own | lobby | room | 例 |
196
+ | ----------- | --------------- | ----- | ----------------- | -------------------- |
197
+ | 個人 app 型 | ownSelf() | なし | sub (本人の部屋) | projects/demo |
198
+ | editor 型 | ownSite(bucket) | 任意 | file の site id | editor services |
199
+ | group 型 | 自作 (D1 join) | 任意 | 所属 group の部屋 | 将来の collaboration |
200
+
201
+ ## 9. これから — 初期化動線の吸収
202
+
203
+ Y.Doc の初期化は「browser で作る (公開閲覧の hydrate)」「DO で作る (draft の復元・demo の投入)」「空から作る (seed)」の 3 経路が排他で並び、ここが service 実装の最も複雑な場所になっている。
204
+ aiax は現時点で境界の契約 (認証認可・room 決定・action 転送) だけを持ち、初期化は service 側に残している。
205
+ 次の段階で、実行場所 (client / worker / DO) と経路 (view / edit / lobby) の場合分けを宣言で表現する boot / persist API をこの library へ吸収する。
206
+
207
+ ## 10. 攻撃面と防御
208
+
209
+ | 攻撃 | 防御 |
210
+ | ---------------------------------------------- | ------------------------------------------------------------------------ |
211
+ | MCP client が room 名を引数で自由指定する | tool 引数に room を持たせない。room は grant の sub/path から own が決定 |
212
+ | 他人の path で grant を得る | mint 前に own で所有を検査。mcp 側でも同じ own で再検査する二重化 |
213
+ | 期限切れ / 改竄された grant | AUTH_SECRET + salt 付き jwt。復号できなければ sub が空になり 401 |
214
+ | x-user-sub header の偽装 | worker が認可済みの値で必ず上書きしてから DO へ渡す |
215
+ | DO への直接アクセス | DO は worker binding 経由でのみ到達可能。公開 URL を持たない |
216
+ | 破壊的 action (公開データの書き換え) | action 実行の直前に own で再認可する実装を service 側で足せる |
217
+ | 認可の掏り替え (client が接続後に path を変更) | grant は { sub, path } 固定。別 path は別 grant の再発行が要る |
218
+
219
+ ## License
220
+
221
+ MIT
@@ -0,0 +1,53 @@
1
+ import * as Y from "yjs";
2
+ import { ZodRawShape } from "zod";
3
+ //#region src/types.d.ts
4
+ type Side = 'server' | 'client' | 'both';
5
+ interface ActMeta {
6
+ name: string;
7
+ description: string;
8
+ input?: ZodRawShape;
9
+ }
10
+ interface ActionDef extends ActMeta {
11
+ side: Side;
12
+ run: (input: any) => Promise<any>;
13
+ }
14
+ interface Grant {
15
+ sub: string;
16
+ path: string;
17
+ }
18
+ type Own = (env: any, sub: string, path: string) => Promise<string> | string;
19
+ interface Cookie {
20
+ apex: string;
21
+ services: string[];
22
+ }
23
+ interface Config {
24
+ name: string;
25
+ own: Own;
26
+ database?: (env: any) => any;
27
+ lobby?: string;
28
+ alias?: string;
29
+ system?: string;
30
+ model?: string;
31
+ claude?: string;
32
+ maxTokens?: number;
33
+ maxTurns?: number;
34
+ binding?: string;
35
+ endpoint?: string;
36
+ version?: string;
37
+ salt?: string;
38
+ maxAge?: number;
39
+ cookie?: Cookie;
40
+ providers?: any[];
41
+ }
42
+ //#endregion
43
+ //#region src/action.d.ts
44
+ type Action<I, R> = (input: I) => R | Promise<R>;
45
+ type Call = (name: string, input: any) => Promise<any>;
46
+ declare const actions: Map<string, ActionDef>;
47
+ declare const side: Side;
48
+ declare const bind: (doc: Y.Doc) => Y.Doc;
49
+ declare const data: <T = any>(name?: string) => Y.Map<T>;
50
+ declare function action<I extends object, R>(load: Action<I, R> | ((input?: I) => any), at: Side, meta: ActMeta): (input: I) => Promise<R>;
51
+ declare const init: (doc: Y.Doc, wrap?: (run: () => Promise<any>) => Promise<any>) => Call;
52
+ //#endregion
53
+ export { data as a, ActMeta as c, Cookie as d, Grant as f, bind as i, ActionDef as l, Side as m, action as n, init as o, Own as p, actions as r, side as s, Call as t, Config as u };
@@ -0,0 +1,2 @@
1
+ import { a as data, i as bind, n as action, o as init, r as actions, s as side, t as Call } from "./action-DItZAgdx.js";
2
+ export { Call, action, actions, bind, data, init, side };
package/dist/action.js ADDED
@@ -0,0 +1 @@
1
+ const e=new Map,t=typeof window>`u`?`server`:`client`;let n,r;const i=e=>r=e,a=(e=`tree`)=>r.getMap(e);function o(t,r,i){let a=async e=>{let n=await t(e);return typeof n==`function`?n(e):n?.default?n.default(e):n};return e.set(i.name,{...i,side:r,run:a}),e=>n(i.name,e)}const s=(r,a=e=>e())=>{i(r);let o=r.getMap(`act-req`),s=r.getMap(`act-res`),c=new Map,l=async n=>{let c=o.get(n),l=e.get(c?.name);!l||l.side!==t||(o.delete(n),s.set(n,{value:await a(()=>(i(r),l.run(c.input)))}))},u=e=>{let t=c.get(e);!t||!s.has(e)||(c.delete(e),t(s.get(e).value),s.delete(e))};return o.observe(e=>e.keysChanged.forEach(l)),s.observe(e=>e.keysChanged.forEach(u)),n=(n,a)=>{let s=e.get(n);return s.side===t||s.side===`both`?(i(r),s.run(a)):new Promise(e=>{let t=crypto.randomUUID();c.set(t,e),o.set(t,{name:n,input:a})})},n};export{o as action,e as actions,i as bind,a as data,s as init,t as side};
@@ -0,0 +1,21 @@
1
+ import { a as data, c as ActMeta, i as bind, l as ActionDef, m as Side, n as action, o as init, r as actions, s as side, t as Call } from "./action-DItZAgdx.js";
2
+ import { n as BINDING, t as ALIAS } from "./const-Hb5KgSHO.js";
3
+ import * as Y from "yjs";
4
+ //#region src/client.d.ts
5
+ interface Route {
6
+ room?: string;
7
+ path?: string;
8
+ host?: string;
9
+ party?: string;
10
+ doc?: Y.Doc;
11
+ }
12
+ interface Link {
13
+ doc: Y.Doc;
14
+ call: Call;
15
+ synced: (fn: () => void) => void;
16
+ leave: () => void;
17
+ }
18
+ declare const onLeave: (fn: () => void) => undefined;
19
+ declare const connect: (route?: Route) => Link;
20
+ //#endregion
21
+ export { ALIAS, type ActMeta, type ActionDef, BINDING, Link, Route, type Side, action, actions, bind, connect, data, init, onLeave, side };
package/dist/client.js ADDED
@@ -0,0 +1 @@
1
+ import{action as e,actions as t,bind as n,data as r,init as i,side as a}from"./action.js";import{n as o,t as s}from"./const-DMb65zjR.js";import c from"y-partyserver/provider";import*as l from"yjs";let u=null,d=new Set;const f=e=>void d.add(e),p=(e={})=>{u?.leave();let{room:t=``,path:n=``,host:r=location.host,party:a=`v1`,doc:o}=e,s=t?new c(r,t,o,{party:a,params:{path:n}}):null,f=s?.doc??o??new l.Doc;return u={doc:f,call:i(f),synced:e=>{if(!s||s.synced)return void e();s.once(`synced`,()=>e())},leave:()=>{d.forEach(e=>e()),d=new Set,s?.destroy()}}};export{s as ALIAS,o as BINDING,e as action,t as actions,n as bind,p as connect,r as data,i as init,f as onLeave,a as side};
@@ -0,0 +1 @@
1
+ const e=`my-room`,t=`x-user-sub`,n=`x-user-path`,r=`authjs.mcp-token`,i=43200,a=`v1`,o=`/api/mcp`,s=`/api/llm`,c=`1.0.0`,l=`https://api.anthropic.com/v1/messages`,u=`claude-sonnet-5`,d=`2023-06-01`,f=`mcp-client-2025-11-20`,p=32e3;export{l as a,i as c,c as d,n as f,u as i,p as l,t as m,a as n,d as o,r as p,f as r,s,e as t,o as u};
@@ -0,0 +1,5 @@
1
+ //#region src/const.d.ts
2
+ declare const ALIAS = "my-room";
3
+ declare const BINDING = "v1";
4
+ //#endregion
5
+ export { BINDING as n, ALIAS as t };
@@ -0,0 +1,22 @@
1
+ import { a as data, c as ActMeta, d as Cookie, f as Grant, i as bind, l as ActionDef, m as Side, n as action, o as init, p as Own, r as actions, s as side, t as Call, u as Config } from "./action-DItZAgdx.js";
2
+ import { n as BINDING, t as ALIAS } from "./const-Hb5KgSHO.js";
3
+ import { AiaxServer } from "./server.js";
4
+ //#region src/auth.d.ts
5
+ declare const ownSite: (bucket: (env: any) => any, meta?: string) => Own;
6
+ declare const ownSelf: () => Own;
7
+ //#endregion
8
+ //#region src/mcp.d.ts
9
+ declare const grantOf: (request: Request) => Grant;
10
+ //#endregion
11
+ //#region src/index.d.ts
12
+ declare const createAiax: (config: Config) => {
13
+ config: Config;
14
+ Server: typeof AiaxServer;
15
+ use: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
16
+ auth: () => import("hono").MiddlewareHandler;
17
+ party: () => import("hono").MiddlewareHandler<any, string, {}, any>;
18
+ mcp: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
19
+ llm: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
20
+ };
21
+ //#endregion
22
+ export { ALIAS, type ActMeta, type ActionDef, AiaxServer, BINDING, type Call, type Config, type Cookie, type Grant, type Own, type Side, action, actions, bind, createAiax, data, grantOf, init, ownSelf, ownSite, side };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import{action as e,actions as t,bind as n,data as r,init as i,side as a}from"./action.js";import{f as o,m as s,n as c,o as l,r as u,t as d}from"./const-DMb65zjR.js";import{AiaxServer as f}from"./server.js";import{createMiddleware as p}from"hono/factory";import m from"@auth/core/providers/google";import{decode as h,encode as g}from"@auth/core/jwt";import{DrizzleAdapter as _}from"@auth/drizzle-adapter";import{getAuthUser as v,initAuthConfig as y}from"@hono/auth-js";import{drizzle as b}from"drizzle-orm/d1";import{env as x}from"hono/adapter";import{streamText as S}from"hono/streaming";import{StreamableHTTPTransport as C}from"@hono/mcp";import{McpServer as w}from"@modelcontextprotocol/sdk/server/mcp.js";import{getServerByName as T,routePartykitRequest as E}from"partyserver";const D=e=>y(t=>{let n=new URL(t.req.url).host,r=!!e.cookie&&e.cookie.services.some(t=>n===`${t}.${e.cookie.apex}`),i={sessionToken:{options:{domain:`.${e.cookie?.apex}`}}},a=e.providers??[m({clientId:t.env.GOOGLE_CLIENT_ID,clientSecret:t.env.GOOGLE_CLIENT_SECRET})];return{...e.database?{adapter:_(b(e.database(t.env)))}:{},providers:a,secret:t.env.AUTH_SECRET,session:{strategy:`jwt`},...r?{cookies:i}:{}}}),O=async e=>(await v(e))?.token?.sub??``,k=(e,t,n,r)=>g({token:{sub:t,path:n},secret:x(e).AUTH_SECRET,salt:r?.salt??`authjs.mcp-token`,maxAge:r?.maxAge??43200}),A=async(e,t)=>{let n=(e.req.header(`Authorization`)??``).replace(/^Bearer\s+/i,``);if(!n)return null;let r=await h({token:n,secret:x(e).AUTH_SECRET,salt:t?.salt??`authjs.mcp-token`}).catch(()=>null);return{sub:r?.sub??``,path:r?.path??``}},j=(e,t=`site`)=>async(n,r,i)=>!r||!i?``:(await e(n).head(`${r}/${i}`))?.customMetadata?.[t]??``,M=()=>(e,t)=>t,N=e=>e.startsWith(`data:`)?JSON.parse(e.slice(5)):null,P=(e,t,n)=>fetch(e.api,{method:`POST`,headers:{"x-api-key":e.key,"anthropic-version":l,"anthropic-beta":u,"content-type":`application/json`},body:JSON.stringify({model:e.model,max_tokens:e.maxTokens,stream:!0,cache_control:{type:`ephemeral`},system:e.system,mcp_servers:[{type:`url`,url:e.url,name:e.name,...t?{authorization_token:t}:{}}],tools:[{type:`mcp_toolset`,mcp_server_name:e.name}],messages:n})}),F=async(e,t)=>{let n=[],r=null,i=0,a=0,o=0,s=``,c=e.body.getReader(),l=new TextDecoder,u=``;for(;;){let{done:e,value:d}=await c.read();if(e)break;u+=l.decode(d,{stream:!0});let f=u.split(`
2
+ `);u=f.pop()??``;for(let e of f){let c=N(e);if(c){if(c.type===`message_start`){let e=c.message?.usage??{};i+=(e.input_tokens??0)+(e.cache_creation_input_tokens??0)+(e.cache_read_input_tokens??0),a+=e.cache_read_input_tokens??0}c.type===`message_delta`&&(o=c.usage?.output_tokens??o,s=c.delta?.stop_reason??s),c.type===`content_block_start`&&(r={...c.content_block},n.push(r),r.type===`mcp_tool_use`&&await t(`\n[tool] ${r.name}\n`)),c.type===`content_block_delta`&&c.delta?.type===`text_delta`&&(r.text=(r.text??``)+c.delta.text,await t(c.delta.text)),c.type===`content_block_delta`&&c.delta?.type===`input_json_delta`&&(r.partial=(r.partial??``)+c.delta.partial_json),c.type===`content_block_delta`&&c.delta?.type===`thinking_delta`&&(r.thinking=(r.thinking??``)+c.delta.thinking),c.type===`content_block_delta`&&c.delta?.type===`signature_delta`&&(r.signature=(r.signature??``)+c.delta.signature),c.type===`content_block_stop`&&r?.partial!==void 0&&(r.input=r.partial?JSON.parse(r.partial):{},delete r.partial)}}}return{blocks:n,input:i,cached:a,output:o,stop:s}},I=async(e,t,n,r,i,a=10)=>{let o=r,s=0,c=0,l=0,u=``;for(let r=0;r<a;r++){let r=await F(o,i);if(s+=r.input,c+=r.cached,l+=r.output,u=r.stop||u,u!==`pause_turn`)break;if(n.push({role:`assistant`,content:r.blocks.filter(e=>e.type!==`thinking`||e.thinking||e.signature)}),o=await P(e,await t(),n),!o.ok){await i(await o.text());break}}await i(`\n[usage] input ${s} (cache ${c}) / output ${l} tokens (${u})\n`)},L=e=>p(async t=>{let n=await O(t),{path:r=``,prompt:i}=await t.req.json(),a=!!e.lobby&&!r;if(!a&&!n)return t.text(`Unauthorized`,401);if(!a&&!await e.own(x(t),n,r))return t.text(`Forbidden`,403);let o=t.req.header(`x-original-host`)??t.req.header(`x-forwarded-host`)??new URL(t.req.url).host,s={key:x(t).ANTHROPIC_API_KEY,api:e.claude??`https://api.anthropic.com/v1/messages`,name:e.name,url:`https://${o}${e.endpoint??`/api/mcp`}`,model:e.model??`claude-sonnet-5`,system:e.system??``,maxTokens:e.maxTokens??32e3},c=()=>a?Promise.resolve(``):k(t,n,r,e),l=[{role:`user`,content:i}],u=await P(s,await c(),l);return u.ok?S(t,t=>I(s,c,l,u,e=>t.write(e),e.maxTurns??10)):t.text(await u.text(),500)}),R=e=>({sub:e.headers.get(`x-user-sub`)??``,path:decodeURIComponent(e.headers.get(`x-user-path`)??``)}),z=async(e,t,n,r,i)=>{let a=await T(e,t),c={[s]:n.sub,[o]:encodeURIComponent(n.path)};return(await a.fetch(`https://internal/act`,{method:`POST`,headers:c,body:JSON.stringify({name:r,input:i})})).json()},B=(e,t)=>p(async n=>{let r=await A(n,e),i=r?.sub??``,a=r?.path??``,o=e.lobby??``;if(r||!o){if(!i)return n.text(`Unauthorized`,401);if(o=await e.own(x(n),i,a),!o)return n.text(`Forbidden`,403)}let s=new w({name:e.name,version:e.version??`1.0.0`}),c=new C;return t.forEach((t,r)=>s.registerTool(r,{description:t.description,inputSchema:{...t.input}},async t=>{let s=await z(x(n)[e.binding??`v1`],o,{sub:i,path:a},r,t);return{content:[{type:`text`,text:JSON.stringify(s)}]}})),await s.connect(c),c.handleRequest(n)}),V=async(e,t,n,r)=>{let i=new Headers(e.req.raw.headers);i.set(s,n),i.set(o,encodeURIComponent(r));let a=new URL(e.req.url),c=a.pathname.split(`/`);c[3]=encodeURIComponent(t),a.pathname=c.join(`/`);let l=new Request(a,new Request(e.req.raw,{headers:i}));return await E(l,x(e))??e.text(`Not Found`,404)},H=e=>p(async t=>{let n=decodeURIComponent(t.req.path.split(`/`)[3]??``),r=t.req.query(`path`)??``,i=await O(t);if(e.lobby&&n===e.lobby)return V(t,n,i,r);if(!i)return t.text(`Unauthorized`,401);let a=await e.own(x(t),i,r);return!a||n!==a&&n!==(e.alias??`my-room`)?t.text(`Forbidden`,403):V(t,a,i,r)}),U=e=>{let n={auth:D(e),party:H(e),mcp:B(e,t),llm:L(e)},r=p(async(t,r)=>{let i=()=>t.req.path.startsWith(`/parties/`)?n.party(t,r):t.req.path===(e.endpoint??`/api/mcp`)?n.mcp(t,r):t.req.path===`/api/llm`&&t.req.method===`POST`?n.llm(t,r):r(),a;if(await n.auth(t,async()=>void(a=await i())),a)return a});return{config:e,Server:f,use:()=>r,auth:()=>n.auth,party:()=>n.party,mcp:()=>n.mcp,llm:()=>n.llm}};export{d as ALIAS,f as AiaxServer,c as BINDING,e as action,t as actions,n as bind,U as createAiax,r as data,R as grantOf,i as init,M as ownSelf,j as ownSite,a as side};
@@ -0,0 +1,11 @@
1
+ import { t as Call } from "./action-DItZAgdx.js";
2
+ import { YServer } from "y-partyserver";
3
+ //#region src/server.d.ts
4
+ declare class AiaxServer extends YServer {
5
+ wrap: (run: () => Promise<any>) => Promise<any>;
6
+ call: Call;
7
+ onAct(_request: Request): Promise<void>;
8
+ onRequest(request: Request): Promise<Response>;
9
+ }
10
+ //#endregion
11
+ export { AiaxServer };
package/dist/server.js ADDED
@@ -0,0 +1 @@
1
+ import{actions as e,bind as t,init as n}from"./action.js";import{YServer as r}from"y-partyserver";var i=class extends r{wrap=e=>e();call=n(this.document,e=>this.wrap(e));async onAct(e){}async onRequest(n){if(t(this.document),n.method!==`POST`)return new Response(`Not Found`,{status:404});let{name:r,input:i}=await n.json();return e.get(r)?.side===`client`?Response.json({ok:!0,result:await this.call(r,i)}):(await this.onAct(n),Response.json({ok:!0,result:await this.wrap(()=>this.call(r,i))}))}};export{i as AiaxServer};
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "ai-ax",
3
+ "author": "tseijp",
4
+ "version": "0.1.0",
5
+ "license": "MIT",
6
+ "description": "AI Agent Experience (AI/AX) library for Hono + Cloudflare. One act surface for both human UI/UX and LLM MCP tools, with authentication, authorization, realtime sync and Claude API relay.",
7
+ "keywords": [
8
+ "hono",
9
+ "cloudflare",
10
+ "mcp",
11
+ "llm",
12
+ "claude",
13
+ "yjs",
14
+ "durable-objects",
15
+ "auth"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/tseijp/ai-ax.git",
20
+ "directory": "packages/core"
21
+ },
22
+ "type": "module",
23
+ "sideEffects": false,
24
+ "main": "./dist/index.js",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ "./package.json": "./package.json",
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./action": {
34
+ "types": "./dist/action.d.ts",
35
+ "default": "./dist/action.js"
36
+ },
37
+ "./server": {
38
+ "types": "./dist/server.d.ts",
39
+ "default": "./dist/server.js"
40
+ },
41
+ "./client": {
42
+ "types": "./dist/client.d.ts",
43
+ "default": "./dist/client.js"
44
+ },
45
+ "./src": {
46
+ "types": "./src/index.ts",
47
+ "default": "./src/index.ts"
48
+ },
49
+ "./src/*": {
50
+ "types": "./src/*.ts",
51
+ "default": "./src/*.ts"
52
+ },
53
+ "./*": {
54
+ "types": "./dist/index.d.ts",
55
+ "default": "./dist/index.js"
56
+ }
57
+ },
58
+ "files": [
59
+ "dist",
60
+ "README.md"
61
+ ],
62
+ "scripts": {
63
+ "build": "tsdown --config ../../tsdown.config.ts"
64
+ },
65
+ "devDependencies": {
66
+ "@auth/core": "0.34.3",
67
+ "@auth/drizzle-adapter": "1.11.2",
68
+ "@hono/auth-js": "1.1.1",
69
+ "@hono/mcp": "0.3.1",
70
+ "@modelcontextprotocol/sdk": "1.29.0",
71
+ "drizzle-orm": "0.45.2",
72
+ "hono": "4.12.30",
73
+ "partyserver": "0.5.8",
74
+ "y-partyserver": "2.2.0",
75
+ "yjs": "13.6.31",
76
+ "zod": "4.4.3"
77
+ },
78
+ "peerDependencies": {
79
+ "@auth/core": ">=0.34.0",
80
+ "@auth/drizzle-adapter": ">=1.0.0",
81
+ "@hono/auth-js": ">=1.0.0",
82
+ "@hono/mcp": ">=0.3.0",
83
+ "@modelcontextprotocol/sdk": ">=1.0.0",
84
+ "drizzle-orm": ">=0.40.0",
85
+ "hono": ">=4.0.0",
86
+ "partyserver": ">=0.5.0",
87
+ "y-partyserver": ">=2.0.0",
88
+ "yjs": ">=13.0.0",
89
+ "zod": ">=3.25.0"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "@auth/drizzle-adapter": {
93
+ "optional": true
94
+ },
95
+ "drizzle-orm": {
96
+ "optional": true
97
+ }
98
+ }
99
+ }