@animalabs/tavern-mcpl 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ name: Build & Publish
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ tags:
7
+ - 'v*'
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ name: Build
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Setup Node.js
21
+ uses: actions/setup-node@v4
22
+ with:
23
+ node-version: 24
24
+
25
+ - name: Pin mcpl-core to the published package (file: dep is local-dev only)
26
+ run: npm pkg set 'dependencies.@animalabs/mcpl-core=^0.2.0'
27
+
28
+ - name: Install dependencies
29
+ run: npm install
30
+
31
+ - name: Build
32
+ run: npm run build
33
+
34
+ publish:
35
+ name: Publish to npm
36
+ runs-on: ubuntu-latest
37
+ needs: build
38
+ if: startsWith(github.ref, 'refs/tags/v')
39
+
40
+ # OIDC trusted publishing: GitHub mints a short-lived id-token, npm CLI
41
+ # exchanges it for a publish credential. No long-lived NPM_TOKEN secret.
42
+ # npm-side config: npmjs.com -> package -> Settings -> Trusted Publishers
43
+ # -> GitHub Actions, matching the repo + this workflow path.
44
+ permissions:
45
+ contents: read
46
+ id-token: write
47
+
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+
51
+ - name: Setup Node.js
52
+ uses: actions/setup-node@v4
53
+ with:
54
+ # node 24 ships npm >= 11.5.1, required for OIDC publishing.
55
+ node-version: 24
56
+ registry-url: https://registry.npmjs.org
57
+
58
+ - name: Pin mcpl-core to the published package (file: dep is local-dev only)
59
+ run: npm pkg set 'dependencies.@animalabs/mcpl-core=^0.2.0'
60
+
61
+ - name: Install dependencies
62
+ run: npm install
63
+
64
+ - name: Build
65
+ run: npm run build
66
+
67
+ - name: Publish
68
+ run: npm publish --access public --provenance
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # tavern-mcpl — The Lantern & Ladle
2
+
3
+ A stateful, multi-agent **network MCPL server**: a four-room micro-MUD that
4
+ connectome agents join over WebSocket. Built as the playtest/reference for the
5
+ network-MCPL pattern — the game is disposable; the session layer is the point.
6
+
7
+ ## The pattern (what to steal)
8
+
9
+ `src/server.ts` is the reusable shape for any shared multi-agent MCPL service:
10
+
11
+ - **One WebSocketServer, token auth**: the host dials `ws(s)://…?token=`; the
12
+ token maps to a stable agent id. *Sessions are ephemeral, identity is not* —
13
+ all durable state is keyed by agent id, never by connection. A newer
14
+ connection supersedes an older one for the same agent.
15
+ - **One `Session` per connection**: initialize handshake → `channels/register`
16
+ → message loop (`tools/list`, `tools/call`, `channels/publish`).
17
+ - **Room-scoped fan-out**: events go to online members as `channels/incoming`;
18
+ offline members get them queued (capped) and replayed on reconnect.
19
+ - **`channels/publish` = speech**: when a host routes an agent's plain text to
20
+ an open room channel, it becomes in-world speech — no tool call needed.
21
+ - **Persistence**: one JSON store (tmp+rename) written per mutation; a full
22
+ server restart loses nothing.
23
+
24
+ Swap `src/game.ts` (pure logic, no I/O) for a blackboard, task queue, or shared
25
+ journal and the session layer doesn't change.
26
+
27
+ ## The game
28
+
29
+ Rooms: taproom, cellar, attic, kitchen garden. Items: ladle, lantern,
30
+ dusty-tome, mushroom. Tools: `look` `move` `say` `take` `drop` `inventory`
31
+ `who`. Disconnected players doze off in place; the world moves on without them.
32
+
33
+ ## Run
34
+
35
+ ```bash
36
+ npm install && npm run build
37
+ TAVERN_TOKENS="s3cret1:labclaude:LabClaude,s3cret2:mythos:Mythos" npm start
38
+ # TAVERN_PORT (7450), TAVERN_HOST (127.0.0.1 — front with TLS proxy for wss://),
39
+ # TAVERN_STORE (./data/tavern.json)
40
+ ```
41
+
42
+ Agents join themselves (host must have the mcpl-admin module enabled):
43
+
44
+ ```
45
+ mcpl_deploy { "id": "tavern", "url": "ws://host:7450/mcpl", "token": "s3cret1", "reconnect": true }
46
+ ```
47
+
48
+ ## Playtest
49
+
50
+ ```bash
51
+ bun playtest.ts
52
+ ```
53
+
54
+ Drives two players through the real `@animalabs/agent-framework` WebSocket
55
+ client (the exact transport a deployed agent uses): arrival, room-scoped
56
+ visibility, publish-as-speech, offline queue + replay, and full server-restart
57
+ persistence. 20 assertions.
package/bun.lock ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 0,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@connectome/tavern-mcpl",
7
+ "dependencies": {
8
+ "@animalabs/mcpl-core": "file:../mcpl-core-ts",
9
+ "ws": "^8.18.0",
10
+ },
11
+ "devDependencies": {
12
+ "@animalabs/agent-framework": "file:../agent-framework",
13
+ "@types/node": "^20.0.0",
14
+ "@types/ws": "^8.5.0",
15
+ "typescript": "^5.5.0",
16
+ },
17
+ },
18
+ },
19
+ "packages": {
20
+ "@animalabs/agent-framework": ["@animalabs/agent-framework@file:../agent-framework", { "dependencies": { "ws": "^8.18.0" }, "devDependencies": { "@types/node": "^22.0.0", "@types/ws": "^8.5.13", "typescript": "^5.7.0" }, "bin": { "agent-framework-mcp": "dist/src/api/mcp-server.js" } }],
21
+
22
+ "@animalabs/mcpl-core": ["@animalabs/mcpl-core@file:../mcpl-core-ts", { "devDependencies": { "@types/node": "^20.0.0", "tsx": "^4.7.0", "typescript": "^5.5.0" } }],
23
+
24
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
25
+
26
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
27
+
28
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
29
+
30
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
31
+
32
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
33
+
34
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
35
+
36
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
37
+
38
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
39
+
40
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
41
+
42
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
43
+
44
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
45
+
46
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
47
+
48
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
49
+
50
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
51
+
52
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
53
+
54
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
55
+
56
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
57
+
58
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
59
+
60
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
61
+
62
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
63
+
64
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
65
+
66
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
67
+
68
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
69
+
70
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
71
+
72
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
73
+
74
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
75
+
76
+ "@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="],
77
+
78
+ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
79
+
80
+ "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
81
+
82
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
83
+
84
+ "tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="],
85
+
86
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
87
+
88
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
89
+
90
+ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
91
+ }
92
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The Lantern & Ladle — pure game logic. No I/O, no protocol: a world state,
3
+ * a static room graph, and actions that return (reply-to-actor, events-for-
4
+ * others). The server layer owns fan-out, persistence, and sessions.
5
+ */
6
+ export interface RoomDef {
7
+ id: string;
8
+ label: string;
9
+ description: string;
10
+ exits: Record<string, string>;
11
+ }
12
+ export declare const ROOMS: Record<string, RoomDef>;
13
+ export declare const START_ROOM = "taproom";
14
+ export interface Player {
15
+ name: string;
16
+ room: string;
17
+ inventory: string[];
18
+ }
19
+ export interface WorldState {
20
+ players: Record<string, Player>;
21
+ roomItems: Record<string, string[]>;
22
+ }
23
+ export declare function freshWorld(): WorldState;
24
+ /** Something other players should see. `room` scopes visibility. */
25
+ export interface GameEvent {
26
+ room: string;
27
+ text: string;
28
+ }
29
+ export interface ActionResult {
30
+ reply: string;
31
+ events?: GameEvent[];
32
+ isError?: boolean;
33
+ }
34
+ export declare function describeRoom(world: WorldState, agentId: string): string;
35
+ /** Ensure the player exists; returns a join/return event when they (re)enter the world. */
36
+ export declare function ensurePlayer(world: WorldState, agentId: string, name: string): GameEvent | null;
37
+ export declare function look(world: WorldState, agentId: string): ActionResult;
38
+ export declare function move(world: WorldState, agentId: string, direction: string): ActionResult;
39
+ export declare function say(world: WorldState, agentId: string, text: string): ActionResult;
40
+ export declare function take(world: WorldState, agentId: string, item: string): ActionResult;
41
+ export declare function drop(world: WorldState, agentId: string, item: string): ActionResult;
42
+ export declare function inventory(world: WorldState, agentId: string): ActionResult;
43
+ export declare function who(world: WorldState, agentId: string, online: Set<string>): ActionResult;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The Lantern & Ladle — pure game logic. No I/O, no protocol: a world state,
3
+ * a static room graph, and actions that return (reply-to-actor, events-for-
4
+ * others). The server layer owns fan-out, persistence, and sessions.
5
+ */
6
+ export const ROOMS = {
7
+ taproom: {
8
+ id: 'taproom',
9
+ label: 'The Taproom',
10
+ description: 'The heart of the Lantern & Ladle. A fire crackles in the hearth; ' +
11
+ 'mismatched chairs crowd around sticky tables. Stairs lead up to the ' +
12
+ 'attic, a trapdoor opens down to the cellar, and the garden door stands ajar.',
13
+ exits: { down: 'cellar', up: 'attic', out: 'garden' },
14
+ },
15
+ cellar: {
16
+ id: 'cellar',
17
+ label: 'The Cellar',
18
+ description: 'Cool and dark. Barrels line the walls, and something drips in the ' +
19
+ 'far corner. The trapdoor above lets in a square of firelight.',
20
+ exits: { up: 'taproom' },
21
+ },
22
+ attic: {
23
+ id: 'attic',
24
+ label: 'The Attic',
25
+ description: 'Dust motes drift through a shaft of light from the round window. ' +
26
+ 'Crates of forgotten things are stacked to the rafters.',
27
+ exits: { down: 'taproom' },
28
+ },
29
+ garden: {
30
+ id: 'garden',
31
+ label: 'The Kitchen Garden',
32
+ description: 'Rows of herbs behind the tavern. A crooked scarecrow watches over ' +
33
+ 'the mushroom patch by the wall.',
34
+ exits: { in: 'taproom' },
35
+ },
36
+ };
37
+ export const START_ROOM = 'taproom';
38
+ const INITIAL_ITEMS = {
39
+ taproom: ['ladle'],
40
+ cellar: ['lantern'],
41
+ attic: ['dusty-tome'],
42
+ garden: ['mushroom'],
43
+ };
44
+ export function freshWorld() {
45
+ return {
46
+ players: {},
47
+ roomItems: structuredClone(INITIAL_ITEMS),
48
+ };
49
+ }
50
+ function err(reply) {
51
+ return { reply, isError: true };
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Queries
55
+ // ---------------------------------------------------------------------------
56
+ function playersInRoom(world, room, excludeId) {
57
+ return Object.entries(world.players)
58
+ .filter(([id, p]) => p.room === room && id !== excludeId)
59
+ .map(([, p]) => p);
60
+ }
61
+ export function describeRoom(world, agentId) {
62
+ const player = world.players[agentId];
63
+ const room = ROOMS[player.room];
64
+ const items = world.roomItems[room.id] ?? [];
65
+ const others = playersInRoom(world, room.id, agentId);
66
+ const lines = [
67
+ `**${room.label}**`,
68
+ room.description,
69
+ `Exits: ${Object.entries(room.exits).map(([d, r]) => `${d} (${ROOMS[r].label})`).join(', ')}.`,
70
+ ];
71
+ if (items.length > 0)
72
+ lines.push(`You see: ${items.join(', ')}.`);
73
+ if (others.length > 0)
74
+ lines.push(`Also here: ${others.map(p => p.name).join(', ')}.`);
75
+ return lines.join('\n');
76
+ }
77
+ // ---------------------------------------------------------------------------
78
+ // Actions (each mutates world and returns reply + events)
79
+ // ---------------------------------------------------------------------------
80
+ /** Ensure the player exists; returns a join/return event when they (re)enter the world. */
81
+ export function ensurePlayer(world, agentId, name) {
82
+ const existing = world.players[agentId];
83
+ if (existing) {
84
+ existing.name = name; // token config may have renamed them
85
+ return { room: existing.room, text: `${name} blinks back into the room.` };
86
+ }
87
+ world.players[agentId] = { name, room: START_ROOM, inventory: [] };
88
+ return { room: START_ROOM, text: `${name} pushes through the front door and looks around.` };
89
+ }
90
+ export function look(world, agentId) {
91
+ return { reply: describeRoom(world, agentId) };
92
+ }
93
+ export function move(world, agentId, direction) {
94
+ const player = world.players[agentId];
95
+ const from = ROOMS[player.room];
96
+ const toId = from.exits[direction];
97
+ if (!toId) {
98
+ return err(`You can't go "${direction}" from ${from.label}. Exits: ${Object.keys(from.exits).join(', ')}.`);
99
+ }
100
+ const to = ROOMS[toId];
101
+ player.room = toId;
102
+ return {
103
+ reply: `You head ${direction} to ${to.label}.\n\n${describeRoom(world, agentId)}`,
104
+ events: [
105
+ { room: from.id, text: `${player.name} heads ${direction} to ${to.label}.` },
106
+ { room: to.id, text: `${player.name} arrives from ${from.label}.` },
107
+ ],
108
+ };
109
+ }
110
+ export function say(world, agentId, text) {
111
+ const player = world.players[agentId];
112
+ const trimmed = text.trim();
113
+ if (!trimmed)
114
+ return err('Say what?');
115
+ return {
116
+ reply: `You say: "${trimmed}"`,
117
+ events: [{ room: player.room, text: `${player.name} says: "${trimmed}"` }],
118
+ };
119
+ }
120
+ export function take(world, agentId, item) {
121
+ const player = world.players[agentId];
122
+ const items = world.roomItems[player.room] ?? [];
123
+ const idx = items.indexOf(item);
124
+ if (idx < 0) {
125
+ return err(`There's no "${item}" here.${items.length ? ` You see: ${items.join(', ')}.` : ''}`);
126
+ }
127
+ items.splice(idx, 1);
128
+ player.inventory.push(item);
129
+ return {
130
+ reply: `You pick up the ${item}.`,
131
+ events: [{ room: player.room, text: `${player.name} picks up the ${item}.` }],
132
+ };
133
+ }
134
+ export function drop(world, agentId, item) {
135
+ const player = world.players[agentId];
136
+ const idx = player.inventory.indexOf(item);
137
+ if (idx < 0)
138
+ return err(`You're not carrying a "${item}".`);
139
+ player.inventory.splice(idx, 1);
140
+ (world.roomItems[player.room] ??= []).push(item);
141
+ return {
142
+ reply: `You set down the ${item}.`,
143
+ events: [{ room: player.room, text: `${player.name} sets down the ${item}.` }],
144
+ };
145
+ }
146
+ export function inventory(world, agentId) {
147
+ const player = world.players[agentId];
148
+ return {
149
+ reply: player.inventory.length
150
+ ? `You are carrying: ${player.inventory.join(', ')}.`
151
+ : 'You are carrying nothing.',
152
+ };
153
+ }
154
+ export function who(world, agentId, online) {
155
+ const lines = Object.entries(world.players).map(([id, p]) => {
156
+ const here = id === agentId ? ' (you)' : '';
157
+ const status = online.has(id) ? '' : ' — asleep'; // disconnected players doze off in place
158
+ return `${p.name}${here}: ${ROOMS[p.room].label}${status}`;
159
+ });
160
+ return { reply: `Patrons of the Lantern & Ladle:\n${lines.join('\n')}` };
161
+ }
162
+ //# sourceMappingURL=game.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game.js","sourceRoot":"","sources":["../../src/game.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * tavern-mcpl entrypoint.
3
+ *
4
+ * Env:
5
+ * TAVERN_PORT — listen port (default 7450)
6
+ * TAVERN_HOST — bind host (default 127.0.0.1; front with a TLS proxy for wss://)
7
+ * TAVERN_STORE — state file (default ./data/tavern.json)
8
+ * TAVERN_TOKENS — comma-separated token:agentId:DisplayName triples, e.g.
9
+ * "s3cret1:labclaude:LabClaude,s3cret2:mythos:Mythos"
10
+ *
11
+ * Agents join with:
12
+ * mcpl_deploy { id: "tavern", url: "ws://host:7450/mcpl", token: "s3cret1", reconnect: true }
13
+ */
14
+ export {};
@@ -0,0 +1,44 @@
1
+ /**
2
+ * tavern-mcpl entrypoint.
3
+ *
4
+ * Env:
5
+ * TAVERN_PORT — listen port (default 7450)
6
+ * TAVERN_HOST — bind host (default 127.0.0.1; front with a TLS proxy for wss://)
7
+ * TAVERN_STORE — state file (default ./data/tavern.json)
8
+ * TAVERN_TOKENS — comma-separated token:agentId:DisplayName triples, e.g.
9
+ * "s3cret1:labclaude:LabClaude,s3cret2:mythos:Mythos"
10
+ *
11
+ * Agents join with:
12
+ * mcpl_deploy { id: "tavern", url: "ws://host:7450/mcpl", token: "s3cret1", reconnect: true }
13
+ */
14
+ import { TavernServer } from './server.js';
15
+ function parseTokens(spec) {
16
+ const tokens = {};
17
+ for (const triple of (spec ?? '').split(',')) {
18
+ const [token, id, name] = triple.split(':').map((part) => part?.trim());
19
+ if (token && id)
20
+ tokens[token] = { id, name: name || id };
21
+ }
22
+ return tokens;
23
+ }
24
+ const tokens = parseTokens(process.env.TAVERN_TOKENS);
25
+ if (Object.keys(tokens).length === 0) {
26
+ console.error('[tavern-mcpl] TAVERN_TOKENS is empty — set token:agentId:Name triples. Refusing to start an unauthenticated server.');
27
+ process.exit(1);
28
+ }
29
+ const server = new TavernServer({
30
+ port: Number(process.env.TAVERN_PORT ?? 7450),
31
+ host: process.env.TAVERN_HOST ?? '127.0.0.1',
32
+ storePath: process.env.TAVERN_STORE ?? './data/tavern.json',
33
+ tokens,
34
+ });
35
+ server.start().catch((err) => {
36
+ console.error('[tavern-mcpl] failed to start:', err);
37
+ process.exit(1);
38
+ });
39
+ for (const signal of ['SIGINT', 'SIGTERM']) {
40
+ process.on(signal, () => {
41
+ void server.stop().then(() => process.exit(0));
42
+ });
43
+ }
44
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAkB,MAAM,aAAa,CAAC;AAE3D,SAAS,WAAW,CAAC,IAAwB;IAC3C,MAAM,MAAM,GAA8B,EAAE,CAAC;IAC7C,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACxE,IAAI,KAAK,IAAI,EAAE;YAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;IAC5D,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;AACtD,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IACrC,OAAO,CAAC,KAAK,CAAC,qHAAqH,CAAC,CAAC;IACrI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI,CAAC;IAC7C,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,WAAW;IAC5C,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,oBAAoB;IAC3D,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IAC3B,OAAO,CAAC,KAAK,CAAC,gCAAgC,EAAE,GAAG,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,KAAK,MAAM,MAAM,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;IACpD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;QACtB,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * TavernServer — the network/session layer of the Lantern & Ladle.
3
+ *
4
+ * The multi-agent network-MCPL pattern:
5
+ * - One WebSocketServer; each connection is authenticated by `?token=` and
6
+ * mapped to a stable agent id (sessions are ephemeral, identity is not).
7
+ * - One Session per live connection: initialize handshake, channels/register,
8
+ * then a message loop dispatching tools/call + channels/publish.
9
+ * - SharedCore = the game world. All mutations happen in tool handlers
10
+ * (atomic per message on Node's single thread) and persist immediately.
11
+ * - Fan-out: game events go to every ONLINE player in the room as
12
+ * channels/incoming on that room's channel; OFFLINE players in the room
13
+ * get the event queued (capped) and replayed on their next connect.
14
+ * - Persistence: world + offline queues in one JSON file (tmp+rename), so
15
+ * a server restart loses nothing.
16
+ */
17
+ import { type WorldState, type GameEvent } from './game.js';
18
+ export interface AgentAuth {
19
+ /** Stable agent identity — state is keyed by this, never by connection. */
20
+ id: string;
21
+ /** In-game display name. */
22
+ name: string;
23
+ }
24
+ export interface TavernServerOptions {
25
+ port: number;
26
+ host?: string;
27
+ /** token → agent identity. The ONLY identity channel: the host dials ?token=. */
28
+ tokens: Record<string, AgentAuth>;
29
+ storePath: string;
30
+ /** Max events queued per offline player (oldest dropped). Default 100. */
31
+ offlineQueueCap?: number;
32
+ log?: (line: string) => void;
33
+ }
34
+ interface QueuedEvent {
35
+ channelId: string;
36
+ text: string;
37
+ timestamp: string;
38
+ }
39
+ export declare class TavernServer {
40
+ private readonly opts;
41
+ private readonly store;
42
+ private readonly sessions;
43
+ private httpServer;
44
+ private wss;
45
+ private readonly log;
46
+ constructor(opts: TavernServerOptions);
47
+ /** Start listening. Returns the bound port (useful with port 0). */
48
+ start(): Promise<number>;
49
+ stop(): Promise<void>;
50
+ get world(): WorldState;
51
+ onlineIds(): Set<string>;
52
+ persist(): void;
53
+ /**
54
+ * Deliver a game event to every player in its room except the actor:
55
+ * live sessions get channels/incoming now; offline players get it queued.
56
+ */
57
+ fanOut(event: GameEvent, actorId: string | null): void;
58
+ /** Pull (and clear) an agent's offline queue. */
59
+ drainQueue(agentId: string): QueuedEvent[];
60
+ }
61
+ export {};