ai-ax 0.4.2 → 0.5.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 CHANGED
@@ -1,221 +1,155 @@
1
- # aiax
1
+ # ai-ax
2
2
 
3
3
  AI Agent Experience (AI/AX) library for Hono + Cloudflare.
4
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
- ```
5
+ ai-ax builds the human experience (UI/UX) and the AI agent experience (AI/AX) of a web app as one surface. An operation is declared exactly once, and that one declaration is a click handler for the person watching the screen and an MCP tool for the LLM at the same time, backed by one realtime state that both of them read and write.
25
6
 
26
- ## 1. 全体構成
7
+ Three ideas carry the library — **one action**, **one tree**, **one road** — and each of them is a mechanism you can point at, not a slogan: one action is a single tool registry that serves the UI, the MCP server and the cross-side transport; one tree is a single yjs map that is rendered as the screen, dumped as the agent's observation and encoded as the persistence snapshot; one road is a single `ai` object that every runtime adapter receives.
27
8
 
28
- 前提は Hono + Cloudflare Workers。realtime 同期は partyserver (Durable Objects)、認証は Auth.js、LLM は Claude API の MCP connector。
9
+ ## Why
29
10
 
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
- ```
11
+ The AI agent has become one more user of the web site. The web, however, was built on the assumption that a human is the one operating it, so when an agent path is bolted onto an app as a second route beside the browser, everything doubles: two operation surfaces, two auth stacks, two sets of bugs, roughly twice the code to manage. The two copies inevitably drift apart, and the drift is usually discovered weeks later in a log.
39
12
 
40
- worker には 4 つの middleware を並べるだけでよい。
13
+ ai-ax removes the second copy instead of maintaining it. The person and the agent read the same state and call the same operations, so the declaration of an operation _is_ its MCP tool, and the implementation lives in exactly one place. Every operation is synchronized in realtime through partyserver, whether it arrived from a browser click or from an MCP request, so the agent's moves appear on the human's screen as they happen.
41
14
 
42
- ```ts
43
- const ax = createAiax({ name: 'demo', own: ownSelf(), database: (e) => e.my_d1_aiax, 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
- ```
15
+ Authentication is the one place where the two users genuinely differ, because an MCP request from the Claude API cannot carry the browser's session. ai-ax closes that gap with a signed token: when the user sends a prompt, the Hono side mints a token and hands it to the Claude API, the token comes back on every MCP request, and the MCP server verifies the signature before executing anything.
46
16
 
47
- ## 2. 認証と認可
17
+ ## What
48
18
 
49
- credential は 2 種類だけ。どちらも AUTH_SECRET で署名された jwt であり、平文の user id が network を渡ることはない。
19
+ The assumed stack is Hono on Cloudflare Workers. Realtime sync is partyserver — the successor of partykit, which was deprecated after becoming a Cloudflare product — running yjs docs on Durable Objects deployed straight to Cloudflare. Authentication is Auth.js, and the LLM is the Claude API with its MCP connector. Hono itself is confined to the `ai-ax/hono` entry, so every other entry stays framework free.
50
20
 
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 経由のみ |
21
+ The whole library is one loop: the browser and the agent join the same room, and every operation — clicked or tool-called — lands on the same doc and syncs back to both.
56
22
 
57
- 認可は own resolver という 1 つの関数に集約される。
23
+ ![diagram](https://r.tsei.jp/ai-ax/diagram.svg)
58
24
 
59
- ```ts
60
- type Own = (env: any, sub: string, path: string) => Promise<string> | string
61
- ```
25
+ ### One action
62
26
 
63
- 「誰 (sub) が、何 (path) に対して、どの部屋 (room) を得るか」を返す。空文字は拒否。
64
- 所有の定義は service ごとに異なるため、resolver の実装は自由に差し替えられる。
27
+ `x(params, config)` builds the `ai` object. The first argument declares typed state keys with their initial values (write `x<Keys>()` when a key should start undefined), and the second argument is the service config.
65
28
 
29
+ <!-- prettier-ignore -->
66
30
  ```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 も同じ型で書ける
31
+ // actions.ts
32
+ import { x } from 'ai-ax'
33
+ import { z } from 'zod'
34
+
35
+ export default x(
36
+ { count: 0 },
37
+ { name: 'counter', tree: 'count is the current value. Observe before acting', own: ({ sub }) => sub },
38
+ )
39
+ .describe('A counting app.', (a) =>
40
+ a
41
+ .action('increment', 'Add 1 to the current value', () => ++ai.count)
42
+ .action('overwrite', 'Overwrite the current value', { count: z.number() }, ({ count }) => (ai.count = count)),
43
+ )
44
+ .client('notify', 'Show an alert in the browser', { text: z.string() }, ({ text }) => alert(text))
45
+ .server('heavy', 'Run heavy work on the server', { src: z.string() }, () => import('./heavy'))
78
46
  ```
79
47
 
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
- ```
48
+ Each definition takes the tool name first, and after that the description, the zod schema and the implementation in any order — each part is recognized by its type, so an inline handler reads hono-like as `(name, schema, fn)` while a dynamic import reads `(name, () => import('./x'), description, schema)` with the schema at the end where the whole list scans best. The implementation is either an inline function or a dynamic import whose default export receives `(input, ai)`, so heavy or browser-incompatible code stays out of the other side's bundle. `.action` runs on whichever side called it, `.client` only in the browser, `.server` only in the Durable Object; when the other side calls it, the request travels over the shared doc and the result travels back, so a tool call from the LLM can open a dialog in the connected browser and return what the user did. `.describe(text, fn)` blocks nest, and their texts concatenate hierarchically into the MCP tool description. `get_tree` is built in — every service exposes the same observation tool, and `config.tree` replaces its description.
49
+
50
+ The declared names and schemas flow into the type of `ai`: `ai.overwrite({ count: 3 })` typechecks, `ai.overwrite(true)` and `ai.missing()` do not.
113
51
 
114
- grant jwt は llm middleware が turn ごとに mint し、Claude API がそのまま /api/mcp へ運ぶ。
115
- LLM は token の中身を選べず、書き換えれば署名が壊れる。
116
- つまり LLM が操作できる部屋は「その瞬間に認可された 1 部屋」だけに封じられる。
52
+ ### One tree
117
53
 
118
- ## 4. room の決定規則
54
+ All shared state lives in a single yjs map. State declared in the first argument is read and written through plain properties — `ai.count = 3` above syncs to every connected browser and to the agent's next `get_tree` — and `ai.data()` exposes the underlying `Y.Map` for apps that key their state by screen paths. `ai.rows(prefix)`, `ai.scan(prefix)` and `ai.wipe(prefix)` query and clear repeated entries, `ai.tx(fn, origin?)` groups writes into one transaction, and `ai.history(options?)` is the undo manager over the same map.
119
55
 
120
- room 名は client にも LLM にも決めさせない。必ず own resolver の結果を使う。
56
+ The tree is also the persistence unit. `config.load` restores a snapshot when the doc first wakes, `config.save` persists one on the update debounce, on a durable alarm and on every disconnect — which is what survives Durable Object hibernation — and `snapshot(doc, keys?)` encodes the whole doc or only the named maps. A `lobby` room skips persistence entirely, so an anonymous top page stays playable without touching storage. State that should stay out of the shared tree and out of snapshots deliberately steps off the road: the `ai-ax/react` entry ships a small atom store (`atom`, `setAtom`, `useAtomValue`) for browser-local values like an IME buffer or an open popup.
121
57
 
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 つだけ開けられる。設定しなければ全経路で認証必須
58
+ ```ts
59
+ x(
60
+ {},
61
+ {
62
+ name: 'notes',
63
+ lobby: 'notes',
64
+ own: async ({ env, sub, path }) => (await env.my_bucket.head(`${sub}/${path}`))?.customMetadata?.site ?? '',
65
+ load: async ({ env, sub, path }) => (await env.my_bucket.get(`${sub}/${path}`))?.arrayBuffer(),
66
+ save: ({ env, sub, path, room }, buf) => env.my_bucket.put(`${sub}/${path}`, buf, { customMetadata: { site: room } }),
67
+ },
68
+ )
69
+ ```
126
70
 
127
- ## 5. action
71
+ ### One road
128
72
 
129
- action は「UI handler が呼ぶ関数」と「MCP tool」の両方になる操作の最小単位。
73
+ The app touches the library through the one `ai` object, and each runtime has exactly one adapter that receives it.
130
74
 
75
+ <!-- prettier-ignore -->
131
76
  ```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
- })
77
+ // index.ts (worker + Durable Object)
78
+ import { Hono } from 'hono'
79
+ import { hono } from 'ai-ax/hono'
80
+ import { PROMPT } from './prompt'
81
+ import ai from './actions'
82
+
83
+ export default new Hono().use('*', hono(ai, { system: PROMPT }))
84
+ export { Server as PartyServer } from 'ai-ax/server'
85
+ ```
86
+
87
+ ```tsx
88
+ // client.tsx (browser)
89
+ import { useClient } from 'ai-ax/react'
90
+ import ai from './actions'
91
+
92
+ const App = () => {
93
+ const { count } = useClient(ai)
94
+ return <button onClick={() => ai.increment()}>{count}</button>
95
+ }
137
96
  ```
138
97
 
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
98
+ `hono(ai, extra?)` routes `/parties/*` to websocket sync, the MCP endpoint, the LLM relay and the Auth.js session, and lets everything else fall through to the app's own routes; `extra` merges server-only config such as the system prompt or an Auth.js `adapter`, so they never enter the browser bundle. `Server` is the Durable Object: it records the owning user across hibernation, serializes action execution, and drives the `load` / `save` hooks. `useClient(ai)` connects once and re-renders on every tree change (`Client` is the component form, `client(ai)` the non-React form), and `hydrate(ai, entries)` fills a local doc from a published JSON snapshot without waking a Durable Object at all.
99
+
100
+ Runtime-only values — Cloudflare bindings, the authorized user, the room — travel the same road instead of module globals: the server injects them with `ai.use({ ... })`, and every implementation receives `ai` as its second argument. `.abstract(name)` declares a function that actions may call before anyone has implemented it, and `.override(name, (input, next, ai) => ...)` supplies or wraps implementations onion-style, where `next(input)` runs whatever was underneath. Overrides intercept tools too, which is how a browser rewrites the variables the Claude API sends before an action runs.
101
+
102
+ `createStream()` from `ai-ax/client` is the browser side of the relay's wire format: `POST` to the relay streams plain text in which a `[tool]` line marks each MCP call and a trailing `[usage]` line closes the turn, and `parse` / `send` / prior-`messages` replay are the pieces an app needs to render and continue conversations without touching the format itself.
103
+
104
+ ### One resolver
105
+
106
+ Authorization concentrates into the single `own` function: given `{ env, sub, path }` it answers which room this user may enter, and an empty string denies. Only two kinds of credentials exist, both jwt signed with `AUTH_SECRET` — a plain user id never crosses the network.
107
+
108
+ | path | credential | issued by | verified by |
109
+ | ----------------------- | ---------------------------- | -------------------------------------------- | ------------------------------------------- |
110
+ | Browser → Worker | session cookie (Auth.js jwt) | `/api/auth` (Google OAuth by default) | the Auth.js session |
111
+ | Claude → Worker MCP | grant jwt `{ sub, path }` | the LLM relay, minted per prompt | signature check before every tool execution |
112
+ | Worker → Durable Object | room name + user headers | the worker overwrites with authorized values | the DO is reachable only through the worker |
113
+
114
+ Browsers always connect to the alias room `my-room`, and the worker rewrites it to the room `own` returned, so no client ever picks a real room name. The grant jwt is minted when the user sends a prompt and is carried by the Claude API back on every MCP request; the LLM cannot choose the token's contents, and rewriting it breaks the signature, so at any moment the agent is sealed inside the one room that was just authorized, for at most `maxAge` seconds.
115
+
116
+ ## How
117
+
118
+ | entry | main exports | where it runs |
119
+ | -------------- | -------------------------------------------------------- | ----------------------- |
120
+ | `ai-ax` | `x` | everywhere (isomorphic) |
121
+ | `ai-ax/hono` | `hono` | worker |
122
+ | `ai-ax/server` | `Server`, `snapshot` | Durable Object |
123
+ | `ai-ax/client` | `client`, `hydrate`, `createStream`, `ALIAS` | browser |
124
+ | `ai-ax/react` | `useClient`, `Client`, `atom`, `setAtom`, `useAtomValue` | browser (React) |
125
+ | `ai-ax/const` | protocol constants (paths, header names) | both |
126
+
127
+ The worker expects a Durable Object binding named `v1` pointing at the exported `Server` class, and the environment needs `AUTH_SECRET`, the OAuth client credentials and `ANTHROPIC_API_KEY`. Config keys of `x(params, config)` and `hono(ai, extra)`:
128
+
129
+ | key | type | default | meaning |
130
+ | --------- | -------------------------- | ----------------------------------------- | ----------------------------------------------------- |
131
+ | name | `string` | `'x'` | service id, becomes the MCP server name |
132
+ | tree | `string` | generic text | description of the built-in `get_tree` tool |
133
+ | own | `(at: Peer) => string` | deny all | authorization resolver, `''` denies |
134
+ | adapter | `(env) => Adapter` | none | Auth.js adapter factory, jwt-only when omitted |
135
+ | lobby | `string` | none | one anonymous room without persistence |
136
+ | load | `(at: Saved) => buf` | none | snapshot restore on first doc load |
137
+ | save | `(at: Saved, buf) => void` | none | snapshot persist on debounce / alarm / disconnect |
138
+ | keys | `string[]` | whole doc | map names included in the snapshot |
139
+ | alias | `string` | `'my-room'` | connection name meaning "my room", rewritten by `own` |
140
+ | system | `string` | `''` | system prompt for the LLM |
141
+ | model | `string` | `'claude-sonnet-5'` | Claude API model id |
142
+ | claude | `string` | `'https://api.anthropic.com/v1/messages'` | Claude API URL, replaceable with an AI Gateway |
143
+ | maxTokens | `number` | `32000` | max_tokens per response |
144
+ | maxTurns | `number` | `10` | upper bound of pause_turn continuations |
145
+ | binding | `string` | `'v1'` | Durable Object binding name |
146
+ | host | `string` | request host | public host the Claude API reaches the MCP through |
147
+ | endpoint | `string` | `'/api/mcp'` | MCP endpoint path |
148
+ | relay | `string` | `'/api/llm'` | LLM relay path |
149
+ | version | `string` | `'1.0.0'` | MCP server version string |
150
+ | salt | `string` | `'authjs.mcp-token'` | grant jwt salt, separates services |
151
+ | maxAge | `number` | `43200` (12h) | grant jwt lifetime in seconds |
152
+ | cookie | `{ apex, services }` | none | services sharing a session cookie across subdomains |
153
+ | providers | `any[]` | Google | Auth.js providers |
154
+
155
+ The `x-user-sub` / `x-user-path` header names and the anthropic-version are internal protocol constants and intentionally not configurable, because both ends of each protocol must change together.
package/dist/client.d.ts CHANGED
@@ -1,49 +1,2 @@
1
- import { ActMeta, ActionDef, Side } from "./types.js";
2
- import { Call, action, actions, bind, data, init, side } from "./action.js";
3
- import { ALIAS, BINDING } from "./const.js";
4
- import * as Y from "yjs";
5
- //#region src/browser/stream.d.ts
6
- interface StreamTool {
7
- server: string;
8
- name: string;
9
- }
10
- interface StreamItem {
11
- text: string;
12
- tools: StreamTool[];
13
- error?: boolean;
14
- }
15
- interface StreamSend {
16
- prompt: string;
17
- path?: string;
18
- model?: string;
19
- messages?: any[];
20
- headers?: Record<string, string>;
21
- }
22
- interface StreamUpdate {
23
- items: StreamItem[];
24
- delta: string;
25
- raw: string;
26
- }
27
- declare const createStream: (endpoint?: string) => {
28
- parse: (raw: string) => StreamItem[];
29
- send: (input: StreamSend, update?: (u: StreamUpdate) => void) => Promise<StreamItem[]>;
30
- };
31
- //#endregion
32
- //#region src/client.d.ts
33
- interface Route {
34
- room?: string;
35
- path?: string;
36
- host?: string;
37
- party?: string;
38
- doc?: Y.Doc;
39
- }
40
- interface Link {
41
- doc: Y.Doc;
42
- call: Call;
43
- synced: (fn: () => void) => void;
44
- leave: () => void;
45
- }
46
- declare const onLeave: (fn: () => void) => undefined;
47
- declare const connect: (route?: Route) => Link;
48
- //#endregion
49
- export { ALIAS, type ActMeta, type ActionDef, BINDING, Link, Route, type Side, type StreamItem, type StreamSend, type StreamTool, type StreamUpdate, action, actions, bind, connect, createStream, data, init, onLeave, side };
1
+ import { a as StreamItem, c as StreamUpdate, i as hydrate, l as createStream, n as Route, o as StreamSend, r as client, s as StreamTool, t as Link } from "./client-BN8W_6y5.js";
2
+ export { Link, Route, type StreamItem, type StreamSend, type StreamTool, type StreamUpdate, client, createStream, hydrate };
package/dist/client.js CHANGED
@@ -1,2 +1,2 @@
1
- import{ALIAS as e,BINDING as t,LLM_PATH as n}from"./const.js";import{action as r,actions as i,bind as a,data as o,init as s,side as c}from"./action.js";import l from"y-partyserver/provider";import*as u from"yjs";const d=/^\[tool\] (?:(\S+) )?(\S+)$/,f=(e=n)=>{let t=e=>{let t=[{text:``,tools:[]}],n=!1;for(let r of e.split(`
2
- `)){if(r.startsWith(`[usage]`))continue;let e=d.exec(r),i=t[t.length-1];if(e){i.tools.push({server:e[1]??``,name:e[2]}),n=!0;continue}if(!(n&&r.trim()===``)){if(n){t.push({text:r,tools:[]}),n=!1;continue}i.text=i.text===``?r:`${i.text}\n${r}`}}return t.map(e=>({...e,text:e.text.trim()})).filter(e=>e.text!==``||e.tools.length>0)};return{parse:t,send:async(n,r)=>{let{headers:i={},...a}=n,o=await fetch(e,{method:`POST`,headers:{"content-type":`application/json`,...i},body:JSON.stringify(a)});if(!o.ok){let e=(await o.text()).replace(/\n/g,` `),t=[{text:e,tools:[],error:!0}];return r?.({items:t,delta:e,raw:e}),t}let s=o.body.getReader(),c=new TextDecoder,l=``;for(;;){let{done:e,value:n}=await s.read();if(e)break;let i=c.decode(n,{stream:!0});l+=i,r?.({items:t(l),delta:i,raw:l})}return t(l)}}};let p=null,m=new Set;const h=e=>void m.add(e),g=(e={})=>{p?.leave();let{room:t=``,path:n=``,host:r=location.host,party:i=`v1`,doc:a}=e,o=t?new l(r,t,a,{party:i,params:{path:n}}):null,c=o?.doc??a??new u.Doc;return p={doc:c,call:s(c),synced(e){if(!o||o.synced)return void e();o.once(`synced`,()=>e())},leave(){m.forEach(e=>e()),m=new Set,o?.destroy()}}};export{e as ALIAS,t as BINDING,r as action,i as actions,a as bind,g as connect,f as createStream,o as data,s as init,h as onLeave,c as side};
1
+ import{init as e}from"./index.js";import{ALIAS as t,LLM_PATH as n}from"./const.js";import*as r from"yjs";import i from"y-partyserver/provider";const a=/^\[tool\] (?:(\S+) )?(\S+)$/,o=(e=n)=>{let t=e=>{let t=[{text:``,tools:[]}],n=!1;for(let r of e.split(`
2
+ `)){if(r.startsWith(`[usage]`))continue;let e=a.exec(r),i=t[t.length-1];if(e){i.tools.push({server:e[1]??``,name:e[2]}),n=!0;continue}if(!(n&&r.trim()===``)){if(n){t.push({text:r,tools:[]}),n=!1;continue}i.text=i.text===``?r:`${i.text}\n${r}`}}return t.map(e=>({...e,text:e.text.trim()})).filter(e=>e.text!==``||e.tools.length>0)};return{parse:t,send:async(n,r)=>{let{headers:i={},...a}=n,o=await fetch(e,{method:`POST`,headers:{"content-type":`application/json`,...i},body:JSON.stringify(a)});if(!o.ok){let e=(await o.text()).replace(/\n/g,` `),t=[{text:e,tools:[],error:!0}];return r?.({items:t,delta:e,raw:e}),t}let s=o.body.getReader(),c=new TextDecoder,l=``;for(;;){let{done:e,value:n}=await s.read();if(e)break;let i=c.decode(n,{stream:!0});l+=i,r?.({items:t(l),delta:i,raw:l})}return t(l)}}};let s=null,c=new Set;const l=(t={})=>{s?.leave();let{room:n=``,path:a=``,host:o=location.host,party:l=`v1`,doc:u}=t,d=n?new i(o,n,u,{party:l,params:{path:a}}):null,f=d?.doc??u??new r.Doc;return s={doc:f,call:e(f),synced(e){if(!d||d.synced)return void e();d.once(`synced`,()=>e())},leave(){c.forEach(e=>e()),c=new Set,d?.destroy()}}},u=new WeakSet,d=(e,n={})=>{if(u.has(e))return e;u.add(e);let r=l({room:t,...n});return e.use({link:r}),e},f=(e,t,n=(e,t)=>t)=>{if(u.has(e))return e;u.add(e);let r=l({});return r.doc.transact(()=>Object.entries(t).forEach(([e,t])=>r.doc.getMap(`tree`).set(e,n(e,t)))),e.use({link:r}),e};export{d as client,o as createStream,f as hydrate};
package/dist/hono.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { n as Ai, r as Config } from "./types-CQLXtCsz.js";
2
+ //#region src/hono.d.ts
3
+ declare const hono: (ai: Ai<any>, extra?: Partial<Config>) => import("hono").MiddlewareHandler<any, string, {}, Response>;
4
+ //#endregion
5
+ export { hono };
package/dist/hono.js ADDED
@@ -0,0 +1,2 @@
1
+ import{actions as e}from"./index.js";import{PATH_HEADER as t,SUB_HEADER as n}from"./const.js";import{createMiddleware as r}from"hono/factory";import i from"@auth/core/providers/google";import{decode as a,encode as o}from"@auth/core/jwt";import{getAuthUser as s,initAuthConfig as c}from"@hono/auth-js";import{env as l}from"hono/adapter";import{streamText as u}from"hono/streaming";import{StreamableHTTPTransport as d}from"@hono/mcp";import{McpServer as f}from"@modelcontextprotocol/sdk/server/mcp.js";import{getServerByName as p,routePartykitRequest as m}from"partyserver";const h=`authjs.mcp-token`,g=e=>c(t=>{let n=new URL(t.req.url).host,r=!!e.cookie&&e.cookie.services.some(t=>n===`${t}.${e.cookie.apex}`),a={sessionToken:{options:{domain:`.${e.cookie?.apex}`}}},o=e.providers??[i({clientId:t.env.GOOGLE_CLIENT_ID,clientSecret:t.env.GOOGLE_CLIENT_SECRET})];return{...e.adapter?{adapter:e.adapter(t.env)}:{},providers:o,secret:t.env.AUTH_SECRET,session:{strategy:`jwt`},...r?{cookies:a}:{}}}),_=async e=>(await s(e))?.token?.sub??``,v=(e,t,n,r)=>o({token:{sub:t,path:n},secret:l(e).AUTH_SECRET,salt:r?.salt??h,maxAge:r?.maxAge??43200}),y=async(e,t)=>{let n=(e.req.header(`Authorization`)??``).replace(/^Bearer\s+/i,``);if(!n)return null;let r=await a({token:n,secret:l(e).AUTH_SECRET,salt:t?.salt??h}).catch(()=>null);return{sub:r?.sub??``,path:r?.path??``}},b=e=>e.startsWith(`data:`)?JSON.parse(e.slice(5)):null,x=(e,t,n)=>fetch(e.api,{method:`POST`,headers:{"x-api-key":e.key,"anthropic-version":`2023-06-01`,"anthropic-beta":`mcp-client-2025-11-20`,"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})}),S=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=b(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.server_name} ${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}},C=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 S(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 x(e,await t(),n),!o.ok){await i(`\n[error] ${(await o.text()).replace(/\n/g,` `)}\n`);break}}await i(`\n[usage] input ${s} (cache ${c}) / output ${l} tokens (${u})\n`)},w=e=>r(async t=>{let n=await _(t),{path:r=``,prompt:i,model:a=``,messages:o=[]}=await t.req.json(),s=!!e.lobby&&!r;if(!s&&!n)return t.text(`Unauthorized`,401);if(!s&&!await(e.own??(()=>``))({env:l(t),sub:n,path:r}))return t.text(`Forbidden`,403);let c=e.host??t.req.header(`x-original-host`)??t.req.header(`x-forwarded-host`)??new URL(t.req.url).host,d={key:l(t).ANTHROPIC_API_KEY,api:e.claude??`https://api.anthropic.com/v1/messages`,name:e.name??`aiax`,url:`https://${c}${e.endpoint??`/api/mcp`}`,model:a||e.model||`claude-sonnet-5`,system:e.system??``,maxTokens:e.maxTokens??32e3},f=()=>s?Promise.resolve(``):v(t,n,r,e),p=[...o,{role:`user`,content:i}],m=await x(d,await f(),p);return m.ok?u(t,t=>C(d,f,p,m,e=>t.write(e),e.maxTurns??10)):t.text(await m.text(),500)}),T=async(e,r,i,a,o)=>{let s=await p(e,r),c={[n]:i.sub,[t]:encodeURIComponent(i.path)};return(await s.fetch(`https://internal/act`,{method:`POST`,headers:c,body:JSON.stringify({name:a,input:o})})).json()},E=t=>r(async n=>{let r=await y(n,t),i=r?.sub??``,a=r?.path??``,o=t.lobby??``;if(r||!o){if(!i)return n.text(`Unauthorized`,401);if(o=await(t.own??(()=>``))({env:l(n),sub:i,path:a}),!o)return n.text(`Forbidden`,403)}let s=new f({name:t.name??`aiax`,version:t.version??`1.0.0`}),c=new d;return e.forEach((e,r)=>s.registerTool(r,{description:e.description,inputSchema:{...e.input}},async e=>{let s=await T(l(n)[t.binding??`v1`],o,{sub:i,path:a},r,e);return{content:[{type:`text`,text:JSON.stringify(s)}]}})),await s.connect(c),c.handleRequest(n)}),D=async(e,r,i,a)=>{let o=new Headers(e.req.raw.headers);o.set(n,i),o.set(t,encodeURIComponent(a));let s=new URL(e.req.url),c=s.pathname.split(`/`);c[3]=encodeURIComponent(r),s.pathname=c.join(`/`);let u=new Request(s,new Request(e.req.raw,{headers:o}));return await m(u,l(e))??e.text(`Not Found`,404)},O=e=>r(async t=>{let n=decodeURIComponent(t.req.path.split(`/`)[3]??``),r=t.req.query(`path`)??``,i=await _(t);if(e.lobby&&n===e.lobby)return D(t,n,i,r);if(!i)return t.text(`Unauthorized`,401);let a=await(e.own??(()=>``))({env:l(t),sub:i,path:r});return!a||n!==a&&n!==(e.alias??`my-room`)?t.text(`Forbidden`,403):D(t,a,i,r)}),k=(e,t={})=>{let n=Object.assign(e.config,t),i={auth:g(n),party:O(n),mcp:E(n),llm:w(n)};return r(async(e,t)=>{let r=()=>e.req.path.startsWith(`/parties/`)?i.party(e,t):e.req.path===(n.endpoint??`/api/mcp`)?i.mcp(e,t):e.req.path===(n.relay??`/api/llm`)&&e.req.method===`POST`?i.llm(e,t):t(),a;if(await i.auth(e,async()=>void(a=await r())),a)return a})};export{k as hono};
package/dist/index.d.ts CHANGED
@@ -1,16 +1,3 @@
1
- import { Config, Own } from "./types.js";
2
- //#region src/worker/auth.d.ts
3
- declare const ownSite: (bucket: (env: any) => any, meta?: string) => Own;
4
- declare const ownSelf: () => Own;
5
- //#endregion
6
- //#region src/index.d.ts
7
- declare const createAiax: (config: Config) => {
8
- config: Config;
9
- all: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
10
- auth: () => import("hono").MiddlewareHandler;
11
- party: () => import("hono").MiddlewareHandler<any, string, {}, any>;
12
- mcp: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
13
- llm: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
14
- };
15
- //#endregion
16
- export { createAiax, ownSelf, ownSite };
1
+ import { a as Define, c as Peer, i as Cookie, l as Saved, n as Ai, o as Grant, r as Config, s as Own, t as ActionDef, u as Side } from "./types-CQLXtCsz.js";
2
+ import { a as current, c as isBound, i as call, l as side, n as actions, o as data, r as bind, s as init, t as Call, u as x } from "./index-DNUoYl4_.js";
3
+ export { type ActionDef, type Ai, Call, type Config, type Cookie, type Define, type Grant, type Own, type Peer, type Saved, type Side, actions, bind, call, current, data, init, isBound, side, x };
package/dist/index.js CHANGED
@@ -1,2 +1 @@
1
- import{PATH_HEADER as e,SUB_HEADER as t}from"./const.js";import{actions as n}from"./action.js";import{createMiddleware as r}from"hono/factory";import i from"@auth/core/providers/google";import{decode as a,encode as o}from"@auth/core/jwt";import{DrizzleAdapter as s}from"@auth/drizzle-adapter";import{getAuthUser as c,initAuthConfig as l}from"@hono/auth-js";import{drizzle as u}from"drizzle-orm/d1";import{env as d}from"hono/adapter";import{streamText as f}from"hono/streaming";import{StreamableHTTPTransport as p}from"@hono/mcp";import{McpServer as m}from"@modelcontextprotocol/sdk/server/mcp.js";import{getServerByName as h,routePartykitRequest as g}from"partyserver";const _=`authjs.mcp-token`,v=e=>l(t=>{let n=new URL(t.req.url).host,r=!!e.cookie&&e.cookie.services.some(t=>n===`${t}.${e.cookie.apex}`),a={sessionToken:{options:{domain:`.${e.cookie?.apex}`}}},o=e.providers??[i({clientId:t.env.GOOGLE_CLIENT_ID,clientSecret:t.env.GOOGLE_CLIENT_SECRET})];return{...e.database?{adapter:s(u(e.database(t.env)))}:{},providers:o,secret:t.env.AUTH_SECRET,session:{strategy:`jwt`},...r?{cookies:a}:{}}}),y=async e=>(await c(e))?.token?.sub??``,b=(e,t,n,r)=>o({token:{sub:t,path:n},secret:d(e).AUTH_SECRET,salt:r?.salt??_,maxAge:r?.maxAge??43200}),x=async(e,t)=>{let n=(e.req.header(`Authorization`)??``).replace(/^Bearer\s+/i,``);if(!n)return null;let r=await a({token:n,secret:d(e).AUTH_SECRET,salt:t?.salt??_}).catch(()=>null);return{sub:r?.sub??``,path:r?.path??``}},S=(e,t=`site`)=>async(n,r,i)=>!r||!i?``:(await e(n).head(`${r}/${i}`))?.customMetadata?.[t]??``,C=()=>(e,t)=>t,w=e=>e.startsWith(`data:`)?JSON.parse(e.slice(5)):null,T=(e,t,n)=>fetch(e.api,{method:`POST`,headers:{"x-api-key":e.key,"anthropic-version":`2023-06-01`,"anthropic-beta":`mcp-client-2025-11-20`,"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})}),E=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=w(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.server_name} ${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}},D=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 E(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 T(e,await t(),n),!o.ok){await i(`\n[error] ${(await o.text()).replace(/\n/g,` `)}\n`);break}}await i(`\n[usage] input ${s} (cache ${c}) / output ${l} tokens (${u})\n`)},O=e=>r(async t=>{let n=await y(t),{path:r=``,prompt:i,model:a=``,messages:o=[]}=await t.req.json(),s=!!e.lobby&&!r;if(!s&&!n)return t.text(`Unauthorized`,401);if(!s&&!await e.own(d(t),n,r))return t.text(`Forbidden`,403);let c=t.req.header(`x-original-host`)??t.req.header(`x-forwarded-host`)??new URL(t.req.url).host,l={key:d(t).ANTHROPIC_API_KEY,api:e.claude??`https://api.anthropic.com/v1/messages`,name:e.name,url:`https://${c}${e.endpoint??`/api/mcp`}`,model:a||e.model||`claude-sonnet-5`,system:e.system??``,maxTokens:e.maxTokens??32e3},u=()=>s?Promise.resolve(``):b(t,n,r,e),p=[...o,{role:`user`,content:i}],m=await T(l,await u(),p);return m.ok?f(t,t=>D(l,u,p,m,e=>t.write(e),e.maxTurns??10)):t.text(await m.text(),500)}),k=async(n,r,i,a,o)=>{let s=await h(n,r),c={[t]:i.sub,[e]:encodeURIComponent(i.path)};return(await s.fetch(`https://internal/act`,{method:`POST`,headers:c,body:JSON.stringify({name:a,input:o})})).json()},A=e=>r(async t=>{let r=await x(t,e),i=r?.sub??``,a=r?.path??``,o=e.lobby??``;if(r||!o){if(!i)return t.text(`Unauthorized`,401);if(o=await e.own(d(t),i,a),!o)return t.text(`Forbidden`,403)}let s=new m({name:e.name,version:e.version??`1.0.0`}),c=new p;return n.forEach((n,r)=>s.registerTool(r,{description:n.description,inputSchema:{...n.input}},async n=>{let s=await k(d(t)[e.binding??`v1`],o,{sub:i,path:a},r,n);return{content:[{type:`text`,text:JSON.stringify(s)}]}})),await s.connect(c),c.handleRequest(t)}),j=async(n,r,i,a)=>{let o=new Headers(n.req.raw.headers);o.set(t,i),o.set(e,encodeURIComponent(a));let s=new URL(n.req.url),c=s.pathname.split(`/`);c[3]=encodeURIComponent(r),s.pathname=c.join(`/`);let l=new Request(s,new Request(n.req.raw,{headers:o}));return await g(l,d(n))??n.text(`Not Found`,404)},M=e=>r(async t=>{let n=decodeURIComponent(t.req.path.split(`/`)[3]??``),r=t.req.query(`path`)??``,i=await y(t);if(e.lobby&&n===e.lobby)return j(t,n,i,r);if(!i)return t.text(`Unauthorized`,401);let a=await e.own(d(t),i,r);return!a||n!==a&&n!==(e.alias??`my-room`)?t.text(`Forbidden`,403):j(t,a,i,r)}),N=e=>{let t={auth:v(e),party:M(e),mcp:A(e),llm:O(e)},n=r(async(n,r)=>{let i=()=>n.req.path.startsWith(`/parties/`)?t.party(n,r):n.req.path===(e.endpoint??`/api/mcp`)?t.mcp(n,r):n.req.path===`/api/llm`&&n.req.method===`POST`?t.llm(n,r):r(),a;if(await t.auth(n,async()=>void(a=await i())),a)return a});return{config:e,all:()=>n,auth:()=>t.auth,party:()=>t.party,mcp:()=>t.mcp,llm:()=>t.llm}};export{N as createAiax,C as ownSelf,S as ownSite};
1
+ import*as e from"yjs";const t=new Map,n=typeof window>`u`?`server`:`client`;let r,i;const a=e=>i=e,o=()=>!!i,s=(e=`tree`)=>i.getMap(e),c=(e,t)=>r(e,t),l=(e,i=e=>e())=>{a(e);let o=e.getMap(`act-req`),s=e.getMap(`act-res`),c=new Map,l=async r=>{let c=o.get(r),l=t.get(c?.name);!l||l.side!==n||(o.delete(r),s.set(r,{value:await i(()=>(a(e),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)),r=(r,i)=>{let s=t.get(r);return s.side===n||s.side===`both`?(a(e),s.run(i)):new Promise(e=>{let t=crypto.randomUUID();c.set(t,e),o.set(t,{name:r,input:i})})},r},u={ai:null},d=()=>u.ai,f=new WeakMap,p=t=>{let n=s();return f.has(n)||f.set(n,new e.UndoManager(n,t)),f.get(n)},m=e=>!!e&&typeof e.parse==`function`,h=e=>m(e)?{value:e}:e,g=(e,t)=>m(e)&&t&&typeof t==`object`&&`value`in t?t.value:t;function _(e={},r={}){let i={},a=new Map,l=new Set,d=[],f=(e,t)=>n=>{let r=a.get(e);return r?r(n,t,y):t(n)},m=e=>(n,...r)=>{let i=r.find(e=>typeof e==`string`)??``,a=r.find(e=>typeof e==`function`),o=r.find(e=>e&&typeof e==`object`),s=async e=>{let t=g(o,e),n=await a(t,y);return typeof n==`function`?n(t,y):n&&n.default?n.default(t,y):n},c=[...d,i].filter(Boolean).join(` `);return t.set(n,{name:n,description:c,side:e,input:h(o),run:f(n,s)}),y},_=e=>()=>Promise.reject(Error(`not implemented: ${e}`)),v={config:r,context:i,side:n,action:m(`both`),client:m(`client`),server:m(`server`),describe(e,t){return t?(d=[...d,e],t(y),d=d.slice(0,-1),y):y},abstract:e=>(l.add(e),y),override:(e,t)=>(a.set(e,t),y),use:e=>(Object.assign(i,e),y),data:e=>s(e),rows(e){let t=[];return s().forEach((n,r)=>r.startsWith(e)&&!r.slice(e.length).includes(`/`)&&t.push(n)),t.sort((e,t)=>(e?.pos??0)-(t?.pos??0))},scan(e){let t=[];return s().forEach((n,r)=>r.startsWith(e)&&t.push(n)),t},wipe(e){let t=[];s().forEach((n,r)=>r.startsWith(e)&&t.push(r)),t.forEach(e=>s().delete(e))},tx:(e,t)=>s().doc.transact(e,t),history:e=>p(e),call:(e,t)=>c(e,t)};t.set(`get_tree`,{name:`get_tree`,description:r.tree??`共有 tree の全 key/value を dump して画面の現在状態を観測する。操作前の観測用`,side:`both`,run:f(`get_tree`,async()=>{let e={};return s().forEach((t,n)=>e[n]=t),e})});let y=new Proxy(v,{get(n,r){if(typeof r!=`string`||r in n)return n[r];if(t.has(r))return e=>c(r,e);if(l.has(r))return f(r,_(r));if(r in i)return i[r];if(r!==`then`&&r!==`toJSON`)return o()?s().get(r)??e[r]:e[r]},set(t,n,r){return typeof n!=`string`||n in t?(t[n]=r,!0):n in i?(i[n]=r,!0):o()?(s().set(n,r),!0):(e[n]=r,!0)}});return u.ai=y,y}export{t as actions,a as bind,c as call,d as current,s as data,l as init,o as isBound,n as side,_ as x};
@@ -0,0 +1,23 @@
1
+ import { n as Route } from "./client-BN8W_6y5.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/react.d.ts
4
+ declare const atom: <T>(initial: T) => {
5
+ value: T;
6
+ listeners: Set<(v: T) => void>;
7
+ };
8
+ type Atom<T> = ReturnType<typeof atom<T>>;
9
+ declare const setAtom: <T>(a: Atom<T>, v: T | ((p: T) => T)) => void;
10
+ declare const useAtomValue: <T>(a: Atom<T>) => T;
11
+ declare const useAtom: <T>(a: Atom<T>) => [T, (v: T) => void];
12
+ declare const AtomValue: <T>({ atom, children }: {
13
+ atom: Atom<T>;
14
+ children: (v: T) => ReactNode;
15
+ }) => ReactNode;
16
+ declare const useClient: <T extends object>(ai: T, route?: Route) => T;
17
+ declare const Client: <T extends object>({ ai, route, children }: {
18
+ ai: T;
19
+ route?: Route;
20
+ children: (ai: T) => ReactNode;
21
+ }) => ReactNode;
22
+ //#endregion
23
+ export { Atom, AtomValue, Client, atom, setAtom, useAtom, useAtomValue, useClient };
package/dist/react.js ADDED
@@ -0,0 +1 @@
1
+ import{useCallback as e,useEffect as t,useState as n}from"react";const r=e=>({value:e,listeners:new Set}),i=(e,t)=>{typeof t==`function`&&(t=t(e.value)),e.value=t,e.listeners.forEach(e=>e(t))},a=e=>{let[r,i]=n(()=>e.value);return t(()=>{let t=e=>i(e);return e.listeners.add(t),r!==e.value&&i(e.value),()=>void e.listeners.delete(t)},[e]),r},o=t=>[a(t),e(e=>i(t,e),[t])],s=({atom:e,children:t})=>t(a(e)),c=(e,r)=>{let[,i]=n(0),a=e.link;return t(()=>{let t=()=>i(e=>e+1);if(!a)return void import(`./client.js`).then(({client:n})=>(n(e,r),t()));let n=a.doc.getMap(`tree`);return n.observe(t),()=>n.unobserve(t)},[a]),e},l=({ai:e,route:t,children:n})=>n(c(e,t));export{s as AtomValue,l as Client,r as atom,i as setAtom,o as useAtom,a as useAtomValue,c as useClient};
package/dist/server.d.ts CHANGED
@@ -1,14 +1,46 @@
1
- import { Grant } from "./types.js";
2
- import { Call } from "./action.js";
3
- import { BINDING } from "./const.js";
1
+ import { o as Grant, r as Config } from "./types-CQLXtCsz.js";
2
+ import { t as Call } from "./index-DNUoYl4_.js";
3
+ import * as Y from "yjs";
4
4
  import { YServer } from "y-partyserver";
5
5
  //#region src/server.d.ts
6
- declare class AiaxServer extends YServer {
6
+ declare const snapshot: (doc: Y.Doc, keys?: string[]) => Uint8Array;
7
+ declare class Server extends YServer {
8
+ static options: {
9
+ hibernate: boolean;
10
+ };
11
+ static callbackOptions: {
12
+ debounceWait: number;
13
+ debounceMaxWait: number;
14
+ };
15
+ env: any;
16
+ ctx: any;
17
+ ai: any;
18
+ sub: string;
19
+ path: string;
20
+ grant: Grant | null;
21
+ queue: Promise<any>;
22
+ hook: (run: () => Promise<any>) => Promise<any>;
7
23
  wrap: (run: () => Promise<any>) => Promise<any>;
8
24
  call: Call;
25
+ conf(): Config;
26
+ isLobby(): boolean;
27
+ saved(grant: Grant): {
28
+ env: any;
29
+ sub: string;
30
+ path: string;
31
+ room: string;
32
+ };
9
33
  grantOf(request: Request): Grant;
10
- onAct(_grant: Grant, _request: Request): Promise<void>;
34
+ owner(): Promise<Grant>;
35
+ remember(sub: string, path: string): Promise<void>;
36
+ onAct(grant: Grant, _request?: Request): Promise<void>;
37
+ onStart(): Promise<void>;
38
+ onConnect(conn: any, ctx: any): Promise<void>;
39
+ onLoad(): Promise<void>;
40
+ onSave(): Promise<void>;
41
+ onAlarm(): Promise<void>;
42
+ onClose(conn: any, code: number, reason: string, wasClean: boolean): Promise<void>;
11
43
  onRequest(request: Request): Promise<Response>;
12
44
  }
13
45
  //#endregion
14
- export { AiaxServer, BINDING, type Grant };
46
+ export { Server, snapshot };
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{BINDING as e}from"./const.js";import{actions as t,bind as n,init as r}from"./action.js";import{YServer as i}from"y-partyserver";var a=class extends i{wrap=e=>e();call=r(this.document,e=>this.wrap(e));grantOf(e){return{sub:e.headers.get(`x-user-sub`)??``,path:decodeURIComponent(e.headers.get(`x-user-path`)??``)}}async onAct(e,t){}async onRequest(e){if(n(this.document),e.method!==`POST`)return new Response(`Not Found`,{status:404});let{name:r,input:i}=await e.json();return t.get(r)?.side===`client`?Response.json({ok:!0,result:await this.call(r,i)}):(await this.onAct(this.grantOf(e),e),Response.json({ok:!0,result:await this.wrap(()=>this.call(r,i))}))}};export{a as AiaxServer,e as BINDING};
1
+ import{actions as e,bind as t,current as n,init as r}from"./index.js";import"./const.js";import*as i from"yjs";import{YServer as a}from"y-partyserver";const o=(e,t)=>{if(!t)return i.encodeStateAsUpdate(e);let n=new i.Doc;return n.transact(()=>{t.forEach(t=>{let r=e.getMap(t),a=n.getMap(t);r.forEach((e,t)=>a.set(t,e instanceof i.AbstractType?e.clone():e))})}),i.encodeStateAsUpdate(n)};var s=class extends a{static options={hibernate:!0};static callbackOptions={debounceWait:6e4,debounceMaxWait:6e4};ai=n();sub=``;path=``;grant=null;queue=Promise.resolve();hook=e=>e();wrap=e=>{let t=this.grant;this.grant=null;let n=async()=>(this.ai.use({env:this.env,room:this.name,...t??await this.owner()}),this.hook(e));return this.queue=this.queue.then(n,n),this.queue};call=r(this.document,e=>this.wrap(e));conf(){return this.ai?.config??{}}isLobby(){let e=this.conf().lobby;return!!e&&this.name===e}saved(e){return{env:this.env,sub:e.sub,path:e.path,room:this.name}}grantOf(e){return{sub:e.headers.get(`x-user-sub`)??``,path:decodeURIComponent(e.headers.get(`x-user-path`)??``)}}async owner(){for(let e of this.getConnections()){let t=e.state;if(t?.sub)return t}return this.sub?{sub:this.sub,path:this.path}:await this.ctx.storage.get(`owner`)??{sub:``,path:``}}async remember(e,t){this.sub=e||this.sub,this.path=t||this.path,e&&await this.ctx.storage.put(`owner`,{sub:e,path:t})}async onAct(e,t){this.grant=e,await this.remember(e.sub,e.path)}async onStart(){await super.onStart(),this.conf().save&&this.document.on(`update`,()=>{this.isLobby()||this.ctx.storage.setAlarm(Date.now()+7e4)})}async onConnect(e,t){let{sub:n,path:r}=this.grantOf(t.request);return e.setState({sub:n,path:r}),await this.remember(n,r),super.onConnect(e,t)}async onLoad(){let e=this.conf().load;if(!e||this.isLobby())return;let t=await e(this.saved(await this.owner()));t&&t.byteLength&&i.applyUpdate(this.document,new Uint8Array(t),`load`)}async onSave(){let e=this.conf().save;if(!e||this.isLobby())return;let t=await this.owner();t.sub&&await e(this.saved(t),o(this.document,this.conf().keys))}async onAlarm(){await this.onSave()}async onClose(e,t,n,r){await super.onClose(e,t,n,r),await this.onSave()}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(this.grantOf(n),n),Response.json({ok:!0,result:await this.wrap(()=>this.call(r,i))}))}};export{s as Server,o as snapshot};
package/dist/types.d.ts CHANGED
@@ -1,42 +1,2 @@
1
- import { ZodRawShape } from "zod";
2
- //#region src/types.d.ts
3
- type Side = 'server' | 'client' | 'both';
4
- interface ActMeta {
5
- name: string;
6
- description: string;
7
- input?: ZodRawShape;
8
- }
9
- interface ActionDef extends ActMeta {
10
- side: Side;
11
- run: (input: any) => Promise<any>;
12
- }
13
- interface Grant {
14
- sub: string;
15
- path: string;
16
- }
17
- type Own = (env: any, sub: string, path: string) => Promise<string> | string;
18
- interface Cookie {
19
- apex: string;
20
- services: string[];
21
- }
22
- interface Config {
23
- name: string;
24
- own: Own;
25
- database?: (env: any) => any;
26
- lobby?: string;
27
- alias?: string;
28
- system?: string;
29
- model?: string;
30
- claude?: string;
31
- maxTokens?: number;
32
- maxTurns?: number;
33
- binding?: string;
34
- endpoint?: string;
35
- version?: string;
36
- salt?: string;
37
- maxAge?: number;
38
- cookie?: Cookie;
39
- providers?: any[];
40
- }
41
- //#endregion
42
- export { ActMeta, ActionDef, Config, Cookie, Grant, Own, Side };
1
+ import { a as Define, c as Peer, i as Cookie, l as Saved, n as Ai, o as Grant, r as Config, s as Own, t as ActionDef, u as Side } from "./types-CQLXtCsz.js";
2
+ export { ActionDef, Ai, Config, Cookie, Define, Grant, Own, Peer, Saved, Side };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-ax",
3
3
  "author": "tseijp",
4
- "version": "0.4.2",
4
+ "version": "0.5.0",
5
5
  "license": "MIT",
6
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
7
  "keywords": [
@@ -30,10 +30,6 @@
30
30
  "types": "./dist/index.d.ts",
31
31
  "default": "./dist/index.js"
32
32
  },
33
- "./action": {
34
- "types": "./dist/action.d.ts",
35
- "default": "./dist/action.js"
36
- },
37
33
  "./client": {
38
34
  "types": "./dist/client.d.ts",
39
35
  "default": "./dist/client.js"
@@ -42,10 +38,22 @@
42
38
  "types": "./dist/const.d.ts",
43
39
  "default": "./dist/const.js"
44
40
  },
41
+ "./hono": {
42
+ "types": "./dist/hono.d.ts",
43
+ "default": "./dist/hono.js"
44
+ },
45
+ "./react": {
46
+ "types": "./dist/react.d.ts",
47
+ "default": "./dist/react.js"
48
+ },
45
49
  "./server": {
46
50
  "types": "./dist/server.d.ts",
47
51
  "default": "./dist/server.js"
48
52
  },
53
+ "./types": {
54
+ "types": "./dist/types.d.ts",
55
+ "default": "./dist/types.js"
56
+ },
49
57
  "./src": {
50
58
  "types": "./src/index.ts",
51
59
  "default": "./src/index.ts"
@@ -57,42 +65,52 @@
57
65
  },
58
66
  "files": [
59
67
  "dist",
68
+ "mermaid",
60
69
  "README.md"
61
70
  ],
62
71
  "scripts": {
63
- "build": "tsdown --config ../../tsdown.config.ts"
72
+ "build": "tsdown --config ../../tsdown.config.ts && rm -f dist/*-*.d.ts",
73
+ "mmd": "for f in concept stack grant; do mmdc -i media/$f.mmd -o media/$f.svg; done"
64
74
  },
65
75
  "devDependencies": {
66
76
  "@auth/core": "0.34.3",
67
- "@auth/drizzle-adapter": "1.11.2",
68
77
  "@hono/auth-js": "1.1.1",
69
78
  "@hono/mcp": "0.3.1",
70
79
  "@modelcontextprotocol/sdk": "1.29.0",
71
- "drizzle-orm": "0.45.2",
80
+ "@types/react": "^19.2.17",
72
81
  "hono": "4.12.30",
73
82
  "partyserver": "0.5.8",
83
+ "react": "^19.2.7",
74
84
  "y-partyserver": "2.2.0",
75
85
  "yjs": "13.6.31",
76
86
  "zod": "4.4.3"
77
87
  },
78
88
  "peerDependencies": {
79
89
  "@auth/core": ">=0.34.0",
80
- "@auth/drizzle-adapter": ">=1.0.0",
81
90
  "@hono/auth-js": ">=1.0.0",
82
91
  "@hono/mcp": ">=0.3.0",
83
92
  "@modelcontextprotocol/sdk": ">=1.0.0",
84
- "drizzle-orm": ">=0.40.0",
85
93
  "hono": ">=4.0.0",
86
94
  "partyserver": ">=0.5.0",
95
+ "react": ">=18.0.0",
87
96
  "y-partyserver": ">=2.0.0",
88
97
  "yjs": ">=13.0.0",
89
98
  "zod": ">=3.25.0"
90
99
  },
91
100
  "peerDependenciesMeta": {
92
- "@auth/drizzle-adapter": {
101
+ "@hono/auth-js": {
102
+ "optional": true
103
+ },
104
+ "@hono/mcp": {
105
+ "optional": true
106
+ },
107
+ "@modelcontextprotocol/sdk": {
108
+ "optional": true
109
+ },
110
+ "hono": {
93
111
  "optional": true
94
112
  },
95
- "drizzle-orm": {
113
+ "react": {
96
114
  "optional": true
97
115
  }
98
116
  }
package/dist/action.d.ts DELETED
@@ -1,13 +0,0 @@
1
- import { ActMeta, ActionDef, Side } from "./types.js";
2
- import * as Y from "yjs";
3
- //#region src/action.d.ts
4
- type Action<I, R> = (input: I) => R | Promise<R>;
5
- type Call = (name: string, input: any) => Promise<any>;
6
- declare const actions: Map<string, ActionDef>;
7
- declare const side: Side;
8
- declare const bind: (doc: Y.Doc) => Y.Doc;
9
- declare const data: <T = any>(name?: string) => Y.Map<T>;
10
- declare function action<I extends object, R>(load: Action<I, R> | ((input?: I) => any), at: Side, meta: ActMeta): (input: I) => Promise<R>;
11
- declare const init: (doc: Y.Doc, wrap?: (run: () => Promise<any>) => Promise<any>) => Call;
12
- //#endregion
13
- export { Call, action, actions, bind, data, init, side };
package/dist/action.js DELETED
@@ -1 +0,0 @@
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};