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 +127 -193
- package/dist/client.d.ts +2 -49
- package/dist/client.js +2 -2
- package/dist/hono.d.ts +5 -0
- package/dist/hono.js +2 -0
- package/dist/index.d.ts +3 -16
- package/dist/index.js +1 -2
- package/dist/react.d.ts +23 -0
- package/dist/react.js +1 -0
- package/dist/server.d.ts +38 -6
- package/dist/server.js +1 -1
- package/dist/types.d.ts +2 -42
- package/package.json +30 -12
- package/dist/action.d.ts +0 -13
- package/dist/action.js +0 -1
package/README.md
CHANGED
|
@@ -1,221 +1,155 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ai-ax
|
|
2
2
|
|
|
3
3
|
AI Agent Experience (AI/AX) library for Hono + Cloudflare.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
## Why
|
|
29
10
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
17
|
+
## What
|
|
48
18
|
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+

|
|
58
24
|
|
|
59
|
-
|
|
60
|
-
type Own = (env: any, sub: string, path: string) => Promise<string> | string
|
|
61
|
-
```
|
|
25
|
+
### One action
|
|
62
26
|
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
115
|
-
LLM は token の中身を選べず、書き換えれば署名が壊れる。
|
|
116
|
-
つまり LLM が操作できる部屋は「その瞬間に認可された 1 部屋」だけに封じられる。
|
|
52
|
+
### One tree
|
|
117
53
|
|
|
118
|
-
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
71
|
+
### One road
|
|
128
72
|
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
|
171
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
|
174
|
-
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
|
182
|
-
|
|
|
183
|
-
|
|
|
184
|
-
|
|
|
185
|
-
|
|
|
186
|
-
|
|
|
187
|
-
|
|
|
188
|
-
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 {
|
|
2
|
-
|
|
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{
|
|
2
|
-
`)){if(r.startsWith(`[usage]`))continue;let e=
|
|
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
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
|
-
|
|
3
|
-
|
|
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
|
|
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};
|
package/dist/react.d.ts
ADDED
|
@@ -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 "./
|
|
3
|
-
import
|
|
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
|
|
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
|
-
|
|
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 {
|
|
46
|
+
export { Server, snapshot };
|
package/dist/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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 {
|
|
2
|
-
|
|
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
|
+
"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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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};
|