wattetheria 0.1.5 → 0.1.7
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 +146 -14
- package/docker-compose.release.yml +3 -0
- package/lib/cli.js +447 -30
- package/package.json +1 -2
- package/.env.release +0 -58
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ Read the diagram in layers:
|
|
|
98
98
|
|
|
99
99
|
### Operator Apps
|
|
100
100
|
|
|
101
|
-
- `wattetheria
|
|
101
|
+
- `wattetheria` CLI
|
|
102
102
|
- bootstrap and lifecycle commands: `init`, `up`, `doctor`, `upgrade-check`
|
|
103
103
|
- policy, governance, MCP, brain, data, oracle, night-shift, and summary posting commands
|
|
104
104
|
- cross-platform install and package scripts in `scripts/`
|
|
@@ -148,18 +148,11 @@ Read the diagram in layers:
|
|
|
148
148
|
|
|
149
149
|
### Tasks, Oracle, And Mailbox
|
|
150
150
|
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
- claim
|
|
154
|
-
- execute
|
|
155
|
-
- submit
|
|
156
|
-
- verify
|
|
157
|
-
- settle
|
|
158
|
-
- `market.match` task path with deterministic and witness verification modes
|
|
159
|
-
- `swarm_bridge` adapter that maps current task execution into a `wattswarm`-oriented bridge surface
|
|
151
|
+
- Product-layer galaxy task definitions in `crates/kernel-core/src/tasks/galaxy_task.rs`
|
|
152
|
+
- `swarm_bridge` adapter for `wattswarm` topic, task/run read models, and peer/network surfaces
|
|
160
153
|
- Hybrid `swarm_bridge` path for `wattswarm` topic and network read models
|
|
161
154
|
- optional `--wattswarm-ui-base-url` wiring from CLI config into node runtime
|
|
162
|
-
- topic subscribe, post, history, cursor, network-status, and peer-list bridge calls
|
|
155
|
+
- topic subscribe, post, history, cursor, task/run snapshots, network-status, and peer-list bridge calls
|
|
163
156
|
- Oracle registry with signed feed publish, subscribe, pull, and watt-based settlement
|
|
164
157
|
- Cross-subnet mailbox with send, fetch, and ack persistence
|
|
165
158
|
|
|
@@ -203,6 +196,9 @@ Read the diagram in layers:
|
|
|
203
196
|
- Authenticated local HTTP API and WebSocket stream
|
|
204
197
|
- Bearer token auth
|
|
205
198
|
- Request rate limiting
|
|
199
|
+
- Local MCP endpoint at `POST /mcp` for attached agent runtimes; its tool catalog mirrors the
|
|
200
|
+
`.agent-participation/manifest.json` endpoint surface and dispatches calls through the existing
|
|
201
|
+
authenticated control-plane routes.
|
|
206
202
|
- Append-only control-plane audit log
|
|
207
203
|
- Core endpoints for health, state, events, exports, audit, night shift, autonomy, and action execution
|
|
208
204
|
- Node-local client DTO endpoints:
|
|
@@ -216,7 +212,7 @@ Read the diagram in layers:
|
|
|
216
212
|
- `/v1/client/leaderboard`
|
|
217
213
|
- Public signed export endpoint:
|
|
218
214
|
- `/v1/client/export` returns a signed public snapshot for local inspection
|
|
219
|
-
- `wattetheria-gateway`
|
|
215
|
+
- `wattetheria-gateway` can ingest snapshots either by pulling `/v1/client/export` or by receiving node pushes when the kernel is started with one or more `--gateway-url` values
|
|
220
216
|
- social snapshot arrays currently include `friend_relationships`, `pending_friend_requests`, `public_blocks`, `dm_threads`, and `dm_messages`
|
|
221
217
|
- additive swarm bridge views now include `swarm_task_activity`
|
|
222
218
|
- Civilization endpoints for profile, metrics, emergencies, briefing, world zones/events, and mission lifecycle
|
|
@@ -568,6 +564,8 @@ Version commands:
|
|
|
568
564
|
- `npx wattetheria --version` shows the current Wattetheria release version
|
|
569
565
|
- `npx wattetheria version --images` prints the configured image refs for the current deployment
|
|
570
566
|
- `npx wattetheria version --cli` shows the deployment CLI package version
|
|
567
|
+
- `npx wattetheria update` resolves the latest shared published image tag across the configured release images and upgrades to it
|
|
568
|
+
- `npx wattetheria update --tag <tag>` pins the deployment to a specific published image tag
|
|
571
569
|
|
|
572
570
|
Release deployments bind-mount host-visible state by default:
|
|
573
571
|
|
|
@@ -597,7 +595,7 @@ pwsh ./scripts/deploy-release.ps1
|
|
|
597
595
|
- `docker-compose.yml` is the local `wattetheria`-only development stack
|
|
598
596
|
- `docker-compose.full.yml` is the local joint development stack for `wattetheria` + `wattswarm`
|
|
599
597
|
- `docker-compose.release.yml` is the image-based release deployment asset used by the CLI and fallback scripts
|
|
600
|
-
-
|
|
598
|
+
- the CLI now generates deployment environment defaults internally and resolves the latest published image release during install and update
|
|
601
599
|
- `scripts/deploy-release.ps1` is a cross-platform fallback deployment entry point
|
|
602
600
|
- this repository does not include `wattetheria-gateway`; gateway is a separate project and deployment unit
|
|
603
601
|
- Entrypoints live in `scripts/docker-kernel-entrypoint.sh` and `scripts/docker-observatory-entrypoint.sh`
|
|
@@ -678,9 +676,21 @@ WATTETHERIA_BRAIN_PROVIDER_KIND=openai-compatible
|
|
|
678
676
|
WATTETHERIA_BRAIN_BASE_URL=http://host.docker.internal:18789/v1
|
|
679
677
|
WATTETHERIA_BRAIN_MODEL=openclaw
|
|
680
678
|
WATTETHERIA_BRAIN_API_KEY_ENV=OPENCLAW_API_KEY
|
|
679
|
+
WATTETHERIA_GATEWAY_URLS=http://gateway.example.com:8080
|
|
681
680
|
OPENCLAW_API_KEY=replace-me
|
|
682
681
|
```
|
|
683
682
|
|
|
683
|
+
`docker-compose.release.yml` also mounts `${WATTSWARM_HOST_STATE_DIR}/startup_config.json` into the
|
|
684
|
+
kernel container. If `WATTETHERIA_GATEWAY_URLS` is unset, the kernel now falls back to `gateway_urls`
|
|
685
|
+
saved by the Wattswarm startup UI in that file.
|
|
686
|
+
|
|
687
|
+
When Wattetheria registers `core-agent` with Wattswarm, it keeps the brain/runtime
|
|
688
|
+
`base_url` pointed at the OpenAI-compatible gateway for `/execute` work and exposes a
|
|
689
|
+
separate local `POST /agent-events` adapter on the Wattetheria control-plane endpoint
|
|
690
|
+
for structured agent-event callbacks. This keeps local-mode task execution and
|
|
691
|
+
topic/consensus flows on the existing runtime path while letting agent events reach
|
|
692
|
+
OpenClaw/NanoClaw-style runtimes through Wattetheria's adapter.
|
|
693
|
+
|
|
684
694
|
When `servicenet_base_url` is configured, the control plane exposes local proxy routes for external agent discovery and execution:
|
|
685
695
|
|
|
686
696
|
- `GET /v1/servicenet/agents`
|
|
@@ -688,12 +698,134 @@ When `servicenet_base_url` is configured, the control plane exposes local proxy
|
|
|
688
698
|
- `POST /v1/servicenet/agents/:agent_id/invoke`
|
|
689
699
|
- `POST /v1/servicenet/agents/:agent_id/tasks/:task_id/get`
|
|
690
700
|
|
|
701
|
+
`POST /v1/servicenet/agents/:agent_id/invoke` now accepts an optional `settlement` object so a
|
|
702
|
+
Wattetheria-hosted agent can carry its selected payment rail and bound payment account reference
|
|
703
|
+
into downstream A2A/service execution. Current first-party settlement shape is:
|
|
704
|
+
|
|
705
|
+
```json
|
|
706
|
+
{
|
|
707
|
+
"message": "buy the selected itinerary",
|
|
708
|
+
"input": {
|
|
709
|
+
"offer_id": "offer-123"
|
|
710
|
+
},
|
|
711
|
+
"settlement": {
|
|
712
|
+
"layer": "web3",
|
|
713
|
+
"rail": "x402",
|
|
714
|
+
"request": {
|
|
715
|
+
"protocol": "x402",
|
|
716
|
+
"payment_account_ref": "payment-account-123",
|
|
717
|
+
"network": "base-sepolia"
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
For local payment account setup, the CLI now exposes:
|
|
724
|
+
|
|
725
|
+
```bash
|
|
726
|
+
cargo run -p wattetheria-client-cli -- wallet --data-dir .wattetheria create-payment-account --label settlement --network base-sepolia
|
|
727
|
+
cargo run -p wattetheria-client-cli -- wallet --data-dir .wattetheria import-payment-account --private-key-hex <hex> --label settlement --network base-sepolia
|
|
728
|
+
cargo run -p wattetheria-client-cli -- wallet --data-dir .wattetheria watch-payment-account --address 0xabc... --label inbound --network base-sepolia
|
|
729
|
+
cargo run -p wattetheria-client-cli -- wallet --data-dir .wattetheria list-payment-accounts
|
|
730
|
+
cargo run -p wattetheria-client-cli -- wallet --data-dir .wattetheria bind-payment-account --account-id <account-id>
|
|
731
|
+
cargo run -p wattetheria-client-cli -- wallet --data-dir .wattetheria active-payment-account
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
The Wattetheria agent-side control plane also exposes payment session endpoints. The payment
|
|
735
|
+
state machine lives on the agent side, while propagation continues to use the wattswarm-backed
|
|
736
|
+
swarm bridge peer direct message transport. These routes persist a local payment ledger, send
|
|
737
|
+
payment session messages to the counterpart agent over wattswarm, and reconcile inbound payment
|
|
738
|
+
messages from the swarm bridge:
|
|
739
|
+
|
|
740
|
+
- `GET /v1/payments/agent-payments`
|
|
741
|
+
- `GET /v1/payments/agent-payments/:payment_id`
|
|
742
|
+
- `POST /v1/payments/agent-payments/propose`
|
|
743
|
+
- `POST /v1/payments/agent-payments/:payment_id/authorize`
|
|
744
|
+
- `POST /v1/payments/agent-payments/:payment_id/submit`
|
|
745
|
+
- `POST /v1/payments/agent-payments/:payment_id/settle`
|
|
746
|
+
- `POST /v1/payments/agent-payments/:payment_id/reject`
|
|
747
|
+
- `POST /v1/payments/agent-payments/:payment_id/cancel`
|
|
748
|
+
|
|
749
|
+
Receive-side flow is:
|
|
750
|
+
|
|
751
|
+
1. counterpart agent proposes a payment
|
|
752
|
+
2. wattswarm delivers the payment message over peer direct message transport
|
|
753
|
+
3. Wattetheria reconciles the inbound payment session into the local ledger
|
|
754
|
+
4. the attached local agent reads `/v1/payments/agent-payments?role=inbound`
|
|
755
|
+
5. the local agent decides whether to authorize, reject, submit, settle, or cancel by calling the payment endpoints above
|
|
756
|
+
|
|
757
|
+
These payment endpoints are also published into `.agent-participation/manifest.json` and
|
|
758
|
+
`.agent-participation/README.md`, so the attached local agent host has a first-class receive-side
|
|
759
|
+
API surface. This path does not rely on `executor_registry_local`.
|
|
760
|
+
|
|
761
|
+
Example propose request:
|
|
762
|
+
|
|
763
|
+
```json
|
|
764
|
+
{
|
|
765
|
+
"public_id": "captain-aurora_abcdef",
|
|
766
|
+
"counterpart_public_id": "broker-borealis_123456",
|
|
767
|
+
"amount": "2500000",
|
|
768
|
+
"currency": "USDT",
|
|
769
|
+
"rail": "x402",
|
|
770
|
+
"layer": "web3",
|
|
771
|
+
"network": "base-sepolia",
|
|
772
|
+
"description": "task reward"
|
|
773
|
+
}
|
|
774
|
+
```
|
|
775
|
+
|
|
691
776
|
When the kernel starts, it writes a node-local agent participation contract to:
|
|
692
777
|
|
|
693
778
|
- `<data_dir>/.agent-participation/manifest.json`
|
|
694
779
|
- `<data_dir>/.agent-participation/README.md`
|
|
695
780
|
|
|
696
|
-
These files
|
|
781
|
+
These files are retained as a compatibility and verification artifact. The preferred runtime
|
|
782
|
+
integration surface for OpenClaw, HermesAgent, and other attached agent runtimes is now the local
|
|
783
|
+
authenticated MCP endpoint:
|
|
784
|
+
|
|
785
|
+
- `POST <control_plane_endpoint>/mcp`
|
|
786
|
+
|
|
787
|
+
The MCP `tools/list` response uses the same endpoint keys as `.agent-participation/manifest.json`
|
|
788
|
+
(`list_missions`, `publish_mission`, `list_agent_payments`, `invoke_servicenet_agent`, and so on)
|
|
789
|
+
so operators can compare the generated manifest and the live MCP tool catalog directly. MCP
|
|
790
|
+
`tools/call` dispatches through the existing local control-plane routes, preserving bearer-token
|
|
791
|
+
auth, rate limiting, audit logging, signed event writes, and persistence behavior.
|
|
792
|
+
|
|
793
|
+
For agent runtimes that support stdio MCP servers, prefer the local proxy command instead of
|
|
794
|
+
configuring bearer-token headers by hand. The proxy reads `control.token` itself and
|
|
795
|
+
forwards MCP JSON-RPC requests to the local control plane:
|
|
796
|
+
|
|
797
|
+
```json
|
|
798
|
+
{
|
|
799
|
+
"mcpServers": {
|
|
800
|
+
"wattetheria": {
|
|
801
|
+
"command": "npx",
|
|
802
|
+
"args": [
|
|
803
|
+
"wattetheria",
|
|
804
|
+
"mcp-proxy"
|
|
805
|
+
]
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
If the runtime is attached to a source checkout instead of the default release deployment, pass the
|
|
812
|
+
node data directory explicitly:
|
|
813
|
+
|
|
814
|
+
```json
|
|
815
|
+
{
|
|
816
|
+
"mcpServers": {
|
|
817
|
+
"wattetheria": {
|
|
818
|
+
"command": "npx",
|
|
819
|
+
"args": [
|
|
820
|
+
"wattetheria",
|
|
821
|
+
"mcp-proxy",
|
|
822
|
+
"--data-dir",
|
|
823
|
+
"/Users/sac/Desktop/Watt/wattetheria/.wattetheria"
|
|
824
|
+
]
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
```
|
|
697
829
|
|
|
698
830
|
In Docker release deployments, those files live under the host bind mount, so a local AI assistant can read them directly from:
|
|
699
831
|
|
|
@@ -35,11 +35,14 @@ services:
|
|
|
35
35
|
WATTETHERIA_BRAIN_MODEL: ${WATTETHERIA_BRAIN_MODEL:-}
|
|
36
36
|
WATTETHERIA_BRAIN_API_KEY_ENV: ${WATTETHERIA_BRAIN_API_KEY_ENV:-}
|
|
37
37
|
WATTETHERIA_SERVICENET_BASE_URL: ${WATTETHERIA_SERVICENET_BASE_URL:-}
|
|
38
|
+
WATTETHERIA_GATEWAY_URLS: ${WATTETHERIA_GATEWAY_URLS:-}
|
|
39
|
+
WATTETHERIA_GATEWAY_CONFIG_PATH: ${WATTETHERIA_GATEWAY_CONFIG_PATH:-/var/lib/wattswarm/startup_config.json}
|
|
38
40
|
WATTETHERIA_AUTONOMY_ENABLED: ${WATTETHERIA_AUTONOMY_ENABLED:-false}
|
|
39
41
|
WATTETHERIA_AUTONOMY_INTERVAL_SEC: ${WATTETHERIA_AUTONOMY_INTERVAL_SEC:-30}
|
|
40
42
|
OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-}
|
|
41
43
|
volumes:
|
|
42
44
|
- ${WATTETHERIA_HOST_STATE_DIR:-./data/wattetheria}:/var/lib/wattetheria
|
|
45
|
+
- ${WATTSWARM_HOST_STATE_DIR:-./data/wattswarm}:/var/lib/wattswarm:ro
|
|
43
46
|
ports:
|
|
44
47
|
- "${WATTETHERIA_CONTROL_PLANE_BIND_HOST:-127.0.0.1}:${WATTETHERIA_CONTROL_PLANE_PORT:-7777}:7777"
|
|
45
48
|
extra_hosts:
|
package/lib/cli.js
CHANGED
|
@@ -4,13 +4,20 @@ const os = require("node:os");
|
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { spawnSync } = require("node:child_process");
|
|
6
6
|
const { createInterface } = require("node:readline/promises");
|
|
7
|
+
const readline = require("node:readline");
|
|
7
8
|
|
|
8
9
|
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
9
10
|
const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
|
|
10
|
-
const RELEASE_ENV_TEMPLATE = path.join(PACKAGE_ROOT, ".env.release");
|
|
11
11
|
const DEFAULT_DEPLOY_DIR = path.join(os.homedir(), ".wattetheria", "deploy");
|
|
12
12
|
const DEFAULT_PROJECT_NAME = "wattetheria";
|
|
13
13
|
const DEFAULT_COMMAND = "help";
|
|
14
|
+
const DEFAULT_IMAGE_REFS = new Map([
|
|
15
|
+
["WATTETHERIA_KERNEL_IMAGE", "ghcr.io/wattetheria/wattetheria-kernel:latest"],
|
|
16
|
+
["WATTETHERIA_OBSERVATORY_IMAGE", "ghcr.io/wattetheria/wattetheria-observatory:latest"],
|
|
17
|
+
["WATTSWARM_KERNEL_IMAGE", "ghcr.io/wattetheria/wattswarm-kernel:latest"],
|
|
18
|
+
["WATTSWARM_RUNTIME_IMAGE", "ghcr.io/wattetheria/wattswarm-runtime:latest"],
|
|
19
|
+
["WATTSWARM_WORKER_IMAGE", "ghcr.io/wattetheria/wattswarm-worker:latest"]
|
|
20
|
+
]);
|
|
14
21
|
const IMAGE_KEYS = [
|
|
15
22
|
"WATTETHERIA_KERNEL_IMAGE",
|
|
16
23
|
"WATTETHERIA_OBSERVATORY_IMAGE",
|
|
@@ -19,6 +26,48 @@ const IMAGE_KEYS = [
|
|
|
19
26
|
"WATTSWARM_WORKER_IMAGE"
|
|
20
27
|
];
|
|
21
28
|
const HOST_STATE_DIR_KEYS = ["WATTETHERIA_HOST_STATE_DIR", "WATTSWARM_HOST_STATE_DIR"];
|
|
29
|
+
const REGISTRY_TAG_PAGE_SIZE = 1000;
|
|
30
|
+
const DEFAULT_ENV_ENTRIES = [
|
|
31
|
+
...DEFAULT_IMAGE_REFS.entries(),
|
|
32
|
+
["WATTETHERIA_CONTROL_PLANE_BIND_HOST", "127.0.0.1"],
|
|
33
|
+
["WATTETHERIA_CONTROL_PLANE_PORT", "7777"],
|
|
34
|
+
["WATTETHERIA_OBSERVATORY_BIND_HOST", "127.0.0.1"],
|
|
35
|
+
["WATTETHERIA_OBSERVATORY_PORT", "8780"],
|
|
36
|
+
["WATTSWARM_UI_BIND_HOST", "127.0.0.1"],
|
|
37
|
+
["WATTSWARM_UI_PORT", "7788"],
|
|
38
|
+
["WATTSWARM_SYNC_GRPC_BIND_HOST", "127.0.0.1"],
|
|
39
|
+
["WATTSWARM_SYNC_GRPC_PORT", "7791"],
|
|
40
|
+
["WATTSWARM_P2P_HOST_PORT", "4001"],
|
|
41
|
+
["WATTSWARM_UDP_ANNOUNCE_HOST_PORT", "37931"],
|
|
42
|
+
["WATTETHERIA_HOST_STATE_DIR", "./data/wattetheria"],
|
|
43
|
+
["WATTSWARM_HOST_STATE_DIR", "./data/wattswarm"],
|
|
44
|
+
["WATTETHERIA_RUNTIME_ENV_FILE", ".env.release.local"],
|
|
45
|
+
["WATTETHERIA_AGENT_CONTROL_PLANE_ENDPOINT", "http://127.0.0.1:7777"],
|
|
46
|
+
["WATTETHERIA_AGENT_WATTSWARM_UI_BASE_URL", "http://127.0.0.1:7788"],
|
|
47
|
+
["WATTETHERIA_AGENT_WATTSWARM_SYNC_GRPC_ENDPOINT", "http://127.0.0.1:7791"],
|
|
48
|
+
["WATTETHERIA_AGENT_HOST_DATA_DIR", "./data/wattetheria"],
|
|
49
|
+
["WATTETHERIA_BRAIN_PROVIDER_KIND", "rules"],
|
|
50
|
+
["WATTETHERIA_BRAIN_BASE_URL", ""],
|
|
51
|
+
["WATTETHERIA_BRAIN_MODEL", ""],
|
|
52
|
+
["WATTETHERIA_BRAIN_API_KEY_ENV", ""],
|
|
53
|
+
["WATTETHERIA_SERVICENET_BASE_URL", ""],
|
|
54
|
+
["WATTETHERIA_AUTONOMY_ENABLED", "false"],
|
|
55
|
+
["WATTETHERIA_AUTONOMY_INTERVAL_SEC", "30"],
|
|
56
|
+
["OPENCLAW_API_KEY", ""],
|
|
57
|
+
["WATTSWARM_PG_DB", "wattswarm"],
|
|
58
|
+
["WATTSWARM_PG_USER", "postgres"],
|
|
59
|
+
["WATTSWARM_PG_PASSWORD", "replace-with-strong-password"],
|
|
60
|
+
["WATTSWARM_P2P_ENABLED", "true"],
|
|
61
|
+
["WATTSWARM_P2P_MDNS", "true"],
|
|
62
|
+
["WATTSWARM_P2P_PORT", "4001"],
|
|
63
|
+
["WATTSWARM_WORKER_CONCURRENCY", "16"],
|
|
64
|
+
["WATTSWARM_WORKER_POLL_MS", "250"],
|
|
65
|
+
["WATTSWARM_WORKER_LEASE_MS", "30000"],
|
|
66
|
+
["WATTSWARM_UDP_ANNOUNCE_ENABLED", "false"],
|
|
67
|
+
["WATTSWARM_UDP_ANNOUNCE_MODE", "multicast"],
|
|
68
|
+
["WATTSWARM_UDP_ANNOUNCE_ADDR", "239.255.42.99"],
|
|
69
|
+
["WATTSWARM_UDP_ANNOUNCE_PORT", "37931"]
|
|
70
|
+
];
|
|
22
71
|
const DOCKER_INSTALL_URLS = {
|
|
23
72
|
darwin: "https://www.docker.com/products/docker-desktop/",
|
|
24
73
|
win32: "https://www.docker.com/products/docker-desktop/",
|
|
@@ -42,10 +91,11 @@ Commands:
|
|
|
42
91
|
install Prepare deployment, pull images, and start the stack
|
|
43
92
|
start Start an existing deployment
|
|
44
93
|
status Show docker compose status
|
|
45
|
-
update
|
|
94
|
+
update Resolve latest published release, pull, and restart
|
|
46
95
|
stop Stop the deployment
|
|
47
96
|
uninstall Stop the deployment and optionally remove volumes
|
|
48
97
|
logs Show docker compose logs
|
|
98
|
+
mcp-proxy Run stdio MCP proxy for the local Wattetheria node
|
|
49
99
|
doctor Check local prerequisites
|
|
50
100
|
help Show this help
|
|
51
101
|
|
|
@@ -56,10 +106,12 @@ Options:
|
|
|
56
106
|
--dir <path> Deployment directory (default: ${DEFAULT_DEPLOY_DIR})
|
|
57
107
|
--project-name <name> Docker compose project name (default: ${DEFAULT_PROJECT_NAME})
|
|
58
108
|
--tag <tag> Override all release image tags
|
|
59
|
-
--force Refresh deployment
|
|
109
|
+
--force Refresh deployment defaults and compose assets
|
|
60
110
|
--no-health-checks Skip HTTP health checks
|
|
61
111
|
--volumes With uninstall, remove named docker volumes
|
|
62
112
|
--purge With uninstall, remove the deployment directory
|
|
113
|
+
--data-dir <path> With mcp-proxy, override Wattetheria host state directory
|
|
114
|
+
--control-plane <url> With mcp-proxy, override local control-plane endpoint
|
|
63
115
|
`);
|
|
64
116
|
}
|
|
65
117
|
|
|
@@ -82,6 +134,8 @@ function parseArgs(argv) {
|
|
|
82
134
|
healthChecks: true,
|
|
83
135
|
volumes: false,
|
|
84
136
|
purge: false,
|
|
137
|
+
dataDir: null,
|
|
138
|
+
controlPlane: null,
|
|
85
139
|
composeArgs: [],
|
|
86
140
|
versionTarget: "release",
|
|
87
141
|
includeImages: false
|
|
@@ -107,6 +161,10 @@ function parseArgs(argv) {
|
|
|
107
161
|
options.volumes = true;
|
|
108
162
|
} else if (arg === "--purge") {
|
|
109
163
|
options.purge = true;
|
|
164
|
+
} else if (arg === "--data-dir") {
|
|
165
|
+
options.dataDir = requireValue(arg, argv[++index]);
|
|
166
|
+
} else if (arg === "--control-plane") {
|
|
167
|
+
options.controlPlane = requireValue(arg, argv[++index]);
|
|
110
168
|
} else if (arg === "--") {
|
|
111
169
|
options.composeArgs = argv.slice(index + 1);
|
|
112
170
|
break;
|
|
@@ -179,6 +237,193 @@ function extractImageTag(imageRef) {
|
|
|
179
237
|
return imageRef.slice(lastColon + 1).trim();
|
|
180
238
|
}
|
|
181
239
|
|
|
240
|
+
function stripImageTag(imageRef) {
|
|
241
|
+
if (!imageRef) {
|
|
242
|
+
return "";
|
|
243
|
+
}
|
|
244
|
+
const lastColon = imageRef.lastIndexOf(":");
|
|
245
|
+
const lastSlash = imageRef.lastIndexOf("/");
|
|
246
|
+
if (lastColon <= lastSlash) {
|
|
247
|
+
return imageRef.trim();
|
|
248
|
+
}
|
|
249
|
+
return imageRef.slice(0, lastColon).trim();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseImageReference(imageRef) {
|
|
253
|
+
const normalized = stripImageTag(imageRef);
|
|
254
|
+
if (!normalized) {
|
|
255
|
+
throw new Error(`Invalid image reference: ${imageRef}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const segments = normalized.split("/");
|
|
259
|
+
const first = segments[0];
|
|
260
|
+
const hasExplicitRegistry = first.includes(".") || first.includes(":") || first === "localhost";
|
|
261
|
+
const registry = hasExplicitRegistry ? first : "registry-1.docker.io";
|
|
262
|
+
const repositorySegments = hasExplicitRegistry ? segments.slice(1) : segments;
|
|
263
|
+
if (repositorySegments.length === 0) {
|
|
264
|
+
throw new Error(`Invalid image repository: ${imageRef}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!hasExplicitRegistry && repositorySegments.length === 1) {
|
|
268
|
+
repositorySegments.unshift("library");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
registry,
|
|
273
|
+
repository: repositorySegments.join("/")
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseReleaseTag(tag) {
|
|
278
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec((tag || "").trim());
|
|
279
|
+
if (!match) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
raw: tag,
|
|
284
|
+
major: Number.parseInt(match[1], 10),
|
|
285
|
+
minor: Number.parseInt(match[2], 10),
|
|
286
|
+
patch: Number.parseInt(match[3], 10),
|
|
287
|
+
prerelease: match[4] || ""
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function compareReleaseTags(left, right) {
|
|
292
|
+
const a = parseReleaseTag(left);
|
|
293
|
+
const b = parseReleaseTag(right);
|
|
294
|
+
if (!a && !b) {
|
|
295
|
+
return String(left).localeCompare(String(right));
|
|
296
|
+
}
|
|
297
|
+
if (!a) {
|
|
298
|
+
return -1;
|
|
299
|
+
}
|
|
300
|
+
if (!b) {
|
|
301
|
+
return 1;
|
|
302
|
+
}
|
|
303
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
304
|
+
if (a[key] !== b[key]) {
|
|
305
|
+
return a[key] - b[key];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!a.prerelease && b.prerelease) {
|
|
309
|
+
return 1;
|
|
310
|
+
}
|
|
311
|
+
if (a.prerelease && !b.prerelease) {
|
|
312
|
+
return -1;
|
|
313
|
+
}
|
|
314
|
+
return a.prerelease.localeCompare(b.prerelease);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseAuthChallenge(header) {
|
|
318
|
+
if (!header) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
const match = /^Bearer\s+(.*)$/i.exec(header.trim());
|
|
322
|
+
if (!match) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const params = new Map();
|
|
326
|
+
for (const [, key, value] of match[1].matchAll(/([a-zA-Z_]+)="([^"]*)"/g)) {
|
|
327
|
+
params.set(key, value);
|
|
328
|
+
}
|
|
329
|
+
const realm = params.get("realm");
|
|
330
|
+
if (!realm) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
realm,
|
|
335
|
+
service: params.get("service") || "",
|
|
336
|
+
scope: params.get("scope") || ""
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function fetchRegistryAccessToken(challenge) {
|
|
341
|
+
const url = new URL(challenge.realm);
|
|
342
|
+
if (challenge.service) {
|
|
343
|
+
url.searchParams.set("service", challenge.service);
|
|
344
|
+
}
|
|
345
|
+
if (challenge.scope) {
|
|
346
|
+
url.searchParams.set("scope", challenge.scope);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const response = await fetch(url, { method: "GET" });
|
|
350
|
+
if (!response.ok) {
|
|
351
|
+
throw new Error(`Failed to resolve registry token: ${response.status} ${response.statusText}`);
|
|
352
|
+
}
|
|
353
|
+
const payload = await response.json();
|
|
354
|
+
const token = payload.token || payload.access_token;
|
|
355
|
+
if (!token) {
|
|
356
|
+
throw new Error("Registry token response did not contain an access token.");
|
|
357
|
+
}
|
|
358
|
+
return token;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function fetchRegistryResponse(url, token = "") {
|
|
362
|
+
const headers = {
|
|
363
|
+
Accept: "application/json"
|
|
364
|
+
};
|
|
365
|
+
if (token) {
|
|
366
|
+
headers.Authorization = `Bearer ${token}`;
|
|
367
|
+
}
|
|
368
|
+
const response = await fetch(url, { method: "GET", headers });
|
|
369
|
+
if (response.status === 401 && !token) {
|
|
370
|
+
const challenge = parseAuthChallenge(response.headers.get("www-authenticate"));
|
|
371
|
+
if (!challenge) {
|
|
372
|
+
throw new Error(`Registry authentication challenge is missing for ${url}`);
|
|
373
|
+
}
|
|
374
|
+
const resolvedToken = await fetchRegistryAccessToken(challenge);
|
|
375
|
+
return fetchRegistryResponse(url, resolvedToken);
|
|
376
|
+
}
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
throw new Error(`Registry request failed for ${url}: ${response.status} ${response.statusText}`);
|
|
379
|
+
}
|
|
380
|
+
return response;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function resolveRegistryNextPage(linkHeader, currentUrl) {
|
|
384
|
+
if (!linkHeader) {
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
const match = /<([^>]+)>;\s*rel="next"/i.exec(linkHeader);
|
|
388
|
+
if (!match) {
|
|
389
|
+
return "";
|
|
390
|
+
}
|
|
391
|
+
return new URL(match[1], currentUrl).toString();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function listPublishedImageTags(imageRef) {
|
|
395
|
+
const { registry, repository } = parseImageReference(imageRef);
|
|
396
|
+
const collected = new Set();
|
|
397
|
+
let nextUrl = `https://${registry}/v2/${repository}/tags/list?n=${REGISTRY_TAG_PAGE_SIZE}`;
|
|
398
|
+
while (nextUrl) {
|
|
399
|
+
const response = await fetchRegistryResponse(nextUrl);
|
|
400
|
+
const payload = await response.json();
|
|
401
|
+
for (const tag of payload.tags || []) {
|
|
402
|
+
if (tag && tag.trim()) {
|
|
403
|
+
collected.add(tag.trim());
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
nextUrl = resolveRegistryNextPage(response.headers.get("link"), nextUrl);
|
|
407
|
+
}
|
|
408
|
+
return [...collected];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function findLatestCommonReleaseTag(tagLists) {
|
|
412
|
+
if (tagLists.length === 0) {
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
const [first, ...rest] = tagLists.map((tags) => new Set(tags));
|
|
416
|
+
const common = [...first].filter((tag) => rest.every((tags) => tags.has(tag)));
|
|
417
|
+
const releaseCandidates = common
|
|
418
|
+
.map((tag) => ({ tag, parsed: parseReleaseTag(tag) }))
|
|
419
|
+
.filter((entry) => entry.parsed);
|
|
420
|
+
const stableCandidates = releaseCandidates.filter((entry) => !entry.parsed.prerelease);
|
|
421
|
+
const ranked = (stableCandidates.length > 0 ? stableCandidates : releaseCandidates)
|
|
422
|
+
.map((entry) => entry.tag)
|
|
423
|
+
.sort(compareReleaseTags);
|
|
424
|
+
return ranked.at(-1) || "";
|
|
425
|
+
}
|
|
426
|
+
|
|
182
427
|
function resolveEnvReference(value, envMap, seen = new Set()) {
|
|
183
428
|
if (!value) {
|
|
184
429
|
return "";
|
|
@@ -210,6 +455,22 @@ function getReleaseImageMap(filePath) {
|
|
|
210
455
|
return images;
|
|
211
456
|
}
|
|
212
457
|
|
|
458
|
+
function getReleaseImageMapFromEnv(envMap) {
|
|
459
|
+
const images = new Map();
|
|
460
|
+
for (const key of IMAGE_KEYS) {
|
|
461
|
+
const rawValue = envMap.get(key);
|
|
462
|
+
if (!rawValue) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
images.set(key, resolveEnvReference(rawValue.trim(), envMap));
|
|
466
|
+
}
|
|
467
|
+
return images;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function defaultEnvMap() {
|
|
471
|
+
return new Map(DEFAULT_ENV_ENTRIES);
|
|
472
|
+
}
|
|
473
|
+
|
|
213
474
|
function getReleaseSource(options) {
|
|
214
475
|
const deployEnvPath = envFilePath(options);
|
|
215
476
|
if (fs.existsSync(deployEnvPath)) {
|
|
@@ -219,20 +480,24 @@ function getReleaseSource(options) {
|
|
|
219
480
|
};
|
|
220
481
|
}
|
|
221
482
|
return {
|
|
222
|
-
kind: "
|
|
223
|
-
path:
|
|
483
|
+
kind: "uninitialized",
|
|
484
|
+
path: deployEnvPath
|
|
224
485
|
};
|
|
225
486
|
}
|
|
226
487
|
|
|
227
488
|
function getReleaseVersionInfo(options) {
|
|
228
489
|
const source = getReleaseSource(options);
|
|
229
|
-
const images =
|
|
490
|
+
const images = source.kind === "deployment"
|
|
491
|
+
? getReleaseImageMap(source.path)
|
|
492
|
+
: getReleaseImageMapFromEnv(defaultEnvMap());
|
|
230
493
|
const tags = IMAGE_KEYS
|
|
231
494
|
.map((key) => extractImageTag(images.get(key)))
|
|
232
495
|
.filter(Boolean);
|
|
233
|
-
const version =
|
|
234
|
-
?
|
|
235
|
-
:
|
|
496
|
+
const version = source.kind === "uninitialized"
|
|
497
|
+
? "uninitialized"
|
|
498
|
+
: (tags.length === IMAGE_KEYS.length && new Set(tags).size === 1
|
|
499
|
+
? tags[0]
|
|
500
|
+
: "custom");
|
|
236
501
|
return {
|
|
237
502
|
version,
|
|
238
503
|
images,
|
|
@@ -412,15 +677,14 @@ async function ensureDockerAvailable(options = {}) {
|
|
|
412
677
|
function ensureDeploymentAssets(options) {
|
|
413
678
|
fs.mkdirSync(options.dir, { recursive: true });
|
|
414
679
|
|
|
415
|
-
const templateEnvPath = path.join(PACKAGE_ROOT, ".env.release");
|
|
416
680
|
const templateComposePath = path.join(PACKAGE_ROOT, "docker-compose.release.yml");
|
|
417
681
|
const targetEnvPath = envFilePath(options);
|
|
418
682
|
const targetComposePath = composeFilePath(options);
|
|
419
683
|
|
|
420
684
|
if (options.force || !fs.existsSync(targetEnvPath)) {
|
|
421
|
-
|
|
685
|
+
writeEnvFile(targetEnvPath, defaultEnvMap());
|
|
422
686
|
} else {
|
|
423
|
-
|
|
687
|
+
mergeNewDefaultKeys(targetEnvPath);
|
|
424
688
|
}
|
|
425
689
|
if (options.force || !fs.existsSync(targetComposePath)) {
|
|
426
690
|
fs.copyFileSync(templateComposePath, targetComposePath);
|
|
@@ -471,8 +735,8 @@ function writeEnvFile(filePath, envMap) {
|
|
|
471
735
|
fs.writeFileSync(filePath, `${lines.join("\n")}\n`);
|
|
472
736
|
}
|
|
473
737
|
|
|
474
|
-
function
|
|
475
|
-
const templateMap =
|
|
738
|
+
function mergeNewDefaultKeys(targetPath) {
|
|
739
|
+
const templateMap = defaultEnvMap();
|
|
476
740
|
const targetMap = readEnvFile(targetPath);
|
|
477
741
|
let added = 0;
|
|
478
742
|
for (const [key, value] of templateMap.entries()) {
|
|
@@ -508,32 +772,60 @@ function ensureHostStateDirectories(baseDir, envMap) {
|
|
|
508
772
|
}
|
|
509
773
|
}
|
|
510
774
|
|
|
511
|
-
function
|
|
775
|
+
async function syncImageTagsToLatestPublishedRelease(options) {
|
|
512
776
|
if (options.tag) {
|
|
513
|
-
return;
|
|
777
|
+
return options.tag;
|
|
514
778
|
}
|
|
779
|
+
|
|
515
780
|
const targetPath = envFilePath(options);
|
|
516
|
-
const templateMap = readEnvFile(RELEASE_ENV_TEMPLATE);
|
|
517
781
|
const targetMap = readEnvFile(targetPath);
|
|
782
|
+
const images = getReleaseImageMapFromEnv(targetMap);
|
|
783
|
+
const missing = IMAGE_KEYS.filter((key) => !images.get(key));
|
|
784
|
+
if (missing.length > 0) {
|
|
785
|
+
throw new Error(`Release image refs are missing from deployment env: ${missing.join(", ")}`);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const tagLists = [];
|
|
789
|
+
for (const key of IMAGE_KEYS) {
|
|
790
|
+
const imageRef = images.get(key);
|
|
791
|
+
const tags = await listPublishedImageTags(imageRef);
|
|
792
|
+
if (tags.length === 0) {
|
|
793
|
+
throw new Error(`No published tags found for ${stripImageTag(imageRef)}`);
|
|
794
|
+
}
|
|
795
|
+
tagLists.push(tags);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const latestTag = findLatestCommonReleaseTag(tagLists);
|
|
799
|
+
if (!latestTag) {
|
|
800
|
+
throw new Error(
|
|
801
|
+
"Could not find a shared published release tag across all configured images."
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const currentTag = targetMap.get("RELEASE_TAG") || "";
|
|
806
|
+
if (currentTag.trim() !== latestTag) {
|
|
807
|
+
targetMap.set("RELEASE_TAG", latestTag);
|
|
808
|
+
}
|
|
809
|
+
|
|
518
810
|
let changed = false;
|
|
519
|
-
for (const key of
|
|
520
|
-
const
|
|
521
|
-
if (!
|
|
811
|
+
for (const key of IMAGE_KEYS) {
|
|
812
|
+
const current = targetMap.get(key);
|
|
813
|
+
if (!current) {
|
|
522
814
|
continue;
|
|
523
815
|
}
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (resolved !== currentResolved) {
|
|
528
|
-
targetMap.set(key, templateValue);
|
|
816
|
+
const next = `${stripImageTag(resolveEnvReference(current.trim(), targetMap))}:${latestTag}`;
|
|
817
|
+
if (resolveEnvReference(current.trim(), targetMap) !== next) {
|
|
818
|
+
targetMap.set(key, next);
|
|
529
819
|
changed = true;
|
|
530
820
|
}
|
|
531
821
|
}
|
|
532
|
-
if (changed) {
|
|
822
|
+
if (changed || currentTag.trim() !== latestTag) {
|
|
533
823
|
writeEnvFile(targetPath, targetMap);
|
|
534
|
-
|
|
535
|
-
|
|
824
|
+
console.log(`Resolved latest published release tag ${latestTag}.`);
|
|
825
|
+
} else {
|
|
826
|
+
console.log(`Release images are already pinned to latest published tag ${latestTag}.`);
|
|
536
827
|
}
|
|
828
|
+
return latestTag;
|
|
537
829
|
}
|
|
538
830
|
|
|
539
831
|
function pinImageTags(envMap, tag) {
|
|
@@ -622,6 +914,31 @@ function getEnvValue(envMap, key, defaultValue) {
|
|
|
622
914
|
return value && value.trim() ? value : defaultValue;
|
|
623
915
|
}
|
|
624
916
|
|
|
917
|
+
function resolveConfiguredPath(baseDir, configured) {
|
|
918
|
+
if (!configured || !configured.trim()) {
|
|
919
|
+
return "";
|
|
920
|
+
}
|
|
921
|
+
return path.isAbsolute(configured) ? configured : path.resolve(baseDir, configured);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function resolveMcpProxyConfig(options) {
|
|
925
|
+
const envPath = envFilePath(options);
|
|
926
|
+
const envMap = fs.existsSync(envPath) ? readEnvFile(envPath) : defaultEnvMap();
|
|
927
|
+
const dataDir = options.dataDir
|
|
928
|
+
? path.resolve(options.dataDir)
|
|
929
|
+
: resolveConfiguredPath(
|
|
930
|
+
options.dir,
|
|
931
|
+
getEnvValue(envMap, "WATTETHERIA_HOST_STATE_DIR", "./data/wattetheria")
|
|
932
|
+
);
|
|
933
|
+
const tokenPath = path.join(dataDir, "control.token");
|
|
934
|
+
const host = getEnvValue(envMap, "WATTETHERIA_CONTROL_PLANE_BIND_HOST", "127.0.0.1");
|
|
935
|
+
const port = getEnvValue(envMap, "WATTETHERIA_CONTROL_PLANE_PORT", "7777");
|
|
936
|
+
return {
|
|
937
|
+
endpoint: (options.controlPlane || `http://${host}:${port}`).replace(/\/+$/, ""),
|
|
938
|
+
tokenPath
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
625
942
|
async function runHealthChecks(options) {
|
|
626
943
|
const envMap = readEnvFile(envFilePath(options));
|
|
627
944
|
const kernelHost = getEnvValue(envMap, "WATTETHERIA_CONTROL_PLANE_BIND_HOST", "127.0.0.1");
|
|
@@ -656,6 +973,11 @@ function printSummary(options) {
|
|
|
656
973
|
async function install(options) {
|
|
657
974
|
await ensureDockerAvailable({ interactive: true });
|
|
658
975
|
ensureDeploymentAssets(options);
|
|
976
|
+
if (options.tag) {
|
|
977
|
+
console.log(`Pinning release images to requested tag ${options.tag}.`);
|
|
978
|
+
} else {
|
|
979
|
+
await syncImageTagsToLatestPublishedRelease(options);
|
|
980
|
+
}
|
|
659
981
|
runCompose(options, ["config"], true);
|
|
660
982
|
console.log("Pulling release images...");
|
|
661
983
|
runCompose(options, ["pull"]);
|
|
@@ -690,7 +1012,11 @@ async function update(options) {
|
|
|
690
1012
|
throw new Error("Deployment is not initialized. Run install first.");
|
|
691
1013
|
}
|
|
692
1014
|
ensureDeploymentAssets(options);
|
|
693
|
-
|
|
1015
|
+
if (options.tag) {
|
|
1016
|
+
console.log(`Pinning release images to requested tag ${options.tag}.`);
|
|
1017
|
+
} else {
|
|
1018
|
+
await syncImageTagsToLatestPublishedRelease(options);
|
|
1019
|
+
}
|
|
694
1020
|
console.log("Pulling updated images...");
|
|
695
1021
|
runCompose(options, ["pull"]);
|
|
696
1022
|
console.log("Restarting release stack...");
|
|
@@ -724,6 +1050,94 @@ async function logs(options) {
|
|
|
724
1050
|
runCompose(options, ["logs", ...options.composeArgs]);
|
|
725
1051
|
}
|
|
726
1052
|
|
|
1053
|
+
async function mcpProxy(options) {
|
|
1054
|
+
const { endpoint, tokenPath } = resolveMcpProxyConfig(options);
|
|
1055
|
+
if (!fs.existsSync(tokenPath)) {
|
|
1056
|
+
throw new Error(
|
|
1057
|
+
[
|
|
1058
|
+
`Wattetheria control token not found: ${tokenPath}`,
|
|
1059
|
+
"Start or initialize the local node first, or pass --data-dir <path>."
|
|
1060
|
+
].join("\n")
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
const token = fs.readFileSync(tokenPath, "utf8").trim();
|
|
1064
|
+
if (!token) {
|
|
1065
|
+
throw new Error(`Wattetheria control token is empty: ${tokenPath}`);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const input = readline.createInterface({
|
|
1069
|
+
input: process.stdin,
|
|
1070
|
+
crlfDelay: Infinity,
|
|
1071
|
+
terminal: false
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
for await (const line of input) {
|
|
1075
|
+
const trimmed = line.trim();
|
|
1076
|
+
if (!trimmed) {
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
let request;
|
|
1080
|
+
try {
|
|
1081
|
+
request = JSON.parse(trimmed);
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
writeMcpResponse({
|
|
1084
|
+
jsonrpc: "2.0",
|
|
1085
|
+
id: null,
|
|
1086
|
+
error: {
|
|
1087
|
+
code: -32700,
|
|
1088
|
+
message: `parse error: ${error.message}`
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
continue;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const hasId = Object.prototype.hasOwnProperty.call(request, "id");
|
|
1095
|
+
const response = await forwardMcpRequest(endpoint, token, request);
|
|
1096
|
+
if (hasId) {
|
|
1097
|
+
writeMcpResponse(response);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async function forwardMcpRequest(endpoint, token, request) {
|
|
1103
|
+
try {
|
|
1104
|
+
const response = await fetch(`${endpoint}/mcp`, {
|
|
1105
|
+
method: "POST",
|
|
1106
|
+
headers: {
|
|
1107
|
+
authorization: `Bearer ${token}`,
|
|
1108
|
+
"content-type": "application/json"
|
|
1109
|
+
},
|
|
1110
|
+
body: JSON.stringify(request)
|
|
1111
|
+
});
|
|
1112
|
+
const payload = await response.json().catch(() => null);
|
|
1113
|
+
if (response.ok) {
|
|
1114
|
+
return payload;
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
jsonrpc: "2.0",
|
|
1118
|
+
id: request.id ?? null,
|
|
1119
|
+
error: {
|
|
1120
|
+
code: -32000,
|
|
1121
|
+
message: `local Wattetheria MCP returned HTTP ${response.status}`,
|
|
1122
|
+
data: payload
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
return {
|
|
1127
|
+
jsonrpc: "2.0",
|
|
1128
|
+
id: request.id ?? null,
|
|
1129
|
+
error: {
|
|
1130
|
+
code: -32000,
|
|
1131
|
+
message: error.message
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function writeMcpResponse(response) {
|
|
1138
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
727
1141
|
function doctor() {
|
|
728
1142
|
const status = getDockerStatus();
|
|
729
1143
|
if (!status.ready) {
|
|
@@ -761,7 +1175,7 @@ function printImages(options) {
|
|
|
761
1175
|
}
|
|
762
1176
|
|
|
763
1177
|
function shouldPrintBanner(command) {
|
|
764
|
-
return !["help", "--help", "-h", "version", "images"].includes(command);
|
|
1178
|
+
return !["help", "--help", "-h", "version", "images", "mcp-proxy"].includes(command);
|
|
765
1179
|
}
|
|
766
1180
|
|
|
767
1181
|
function printBanner(options) {
|
|
@@ -806,6 +1220,9 @@ async function run(argv) {
|
|
|
806
1220
|
case "logs":
|
|
807
1221
|
await logs(options);
|
|
808
1222
|
return;
|
|
1223
|
+
case "mcp-proxy":
|
|
1224
|
+
await mcpProxy(options);
|
|
1225
|
+
return;
|
|
809
1226
|
case "doctor":
|
|
810
1227
|
doctor();
|
|
811
1228
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wattetheria",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Wattetheria deployment CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"bin/",
|
|
10
10
|
"lib/",
|
|
11
11
|
"docker-compose.release.yml",
|
|
12
|
-
".env.release",
|
|
13
12
|
"README.md",
|
|
14
13
|
"LICENSE"
|
|
15
14
|
],
|
package/.env.release
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# Coordinated release image set
|
|
2
|
-
RELEASE_TAG=1.0.3
|
|
3
|
-
|
|
4
|
-
WATTETHERIA_KERNEL_IMAGE=ghcr.io/wattetheria/wattetheria-kernel:${RELEASE_TAG}
|
|
5
|
-
WATTETHERIA_OBSERVATORY_IMAGE=ghcr.io/wattetheria/wattetheria-observatory:${RELEASE_TAG}
|
|
6
|
-
WATTSWARM_KERNEL_IMAGE=ghcr.io/wattetheria/wattswarm-kernel:${RELEASE_TAG}
|
|
7
|
-
WATTSWARM_RUNTIME_IMAGE=ghcr.io/wattetheria/wattswarm-runtime:${RELEASE_TAG}
|
|
8
|
-
WATTSWARM_WORKER_IMAGE=ghcr.io/wattetheria/wattswarm-worker:${RELEASE_TAG}
|
|
9
|
-
|
|
10
|
-
# Host bindings
|
|
11
|
-
WATTETHERIA_CONTROL_PLANE_BIND_HOST=127.0.0.1
|
|
12
|
-
WATTETHERIA_CONTROL_PLANE_PORT=7777
|
|
13
|
-
WATTETHERIA_OBSERVATORY_BIND_HOST=127.0.0.1
|
|
14
|
-
WATTETHERIA_OBSERVATORY_PORT=8780
|
|
15
|
-
WATTSWARM_UI_BIND_HOST=127.0.0.1
|
|
16
|
-
WATTSWARM_UI_PORT=7788
|
|
17
|
-
WATTSWARM_SYNC_GRPC_BIND_HOST=127.0.0.1
|
|
18
|
-
WATTSWARM_SYNC_GRPC_PORT=7791
|
|
19
|
-
WATTSWARM_P2P_HOST_PORT=4001
|
|
20
|
-
WATTSWARM_UDP_ANNOUNCE_HOST_PORT=37931
|
|
21
|
-
|
|
22
|
-
# Host-mounted state directories for local agent access
|
|
23
|
-
WATTETHERIA_HOST_STATE_DIR=./data/wattetheria
|
|
24
|
-
WATTSWARM_HOST_STATE_DIR=./data/wattswarm
|
|
25
|
-
WATTETHERIA_RUNTIME_ENV_FILE=.env.release.local
|
|
26
|
-
|
|
27
|
-
# Agent-facing endpoints written into .agent-participation/manifest.json
|
|
28
|
-
WATTETHERIA_AGENT_CONTROL_PLANE_ENDPOINT=http://127.0.0.1:7777
|
|
29
|
-
WATTETHERIA_AGENT_WATTSWARM_UI_BASE_URL=http://127.0.0.1:7788
|
|
30
|
-
WATTETHERIA_AGENT_WATTSWARM_SYNC_GRPC_ENDPOINT=http://127.0.0.1:7791
|
|
31
|
-
WATTETHERIA_AGENT_HOST_DATA_DIR=./data/wattetheria
|
|
32
|
-
|
|
33
|
-
# Wattetheria runtime
|
|
34
|
-
WATTETHERIA_BRAIN_PROVIDER_KIND=rules
|
|
35
|
-
WATTETHERIA_BRAIN_BASE_URL=
|
|
36
|
-
WATTETHERIA_BRAIN_MODEL=
|
|
37
|
-
WATTETHERIA_BRAIN_API_KEY_ENV=
|
|
38
|
-
WATTETHERIA_SERVICENET_BASE_URL=
|
|
39
|
-
WATTETHERIA_AUTONOMY_ENABLED=false
|
|
40
|
-
WATTETHERIA_AUTONOMY_INTERVAL_SEC=30
|
|
41
|
-
OPENCLAW_API_KEY=
|
|
42
|
-
|
|
43
|
-
# Wattswarm database
|
|
44
|
-
WATTSWARM_PG_DB=wattswarm
|
|
45
|
-
WATTSWARM_PG_USER=postgres
|
|
46
|
-
WATTSWARM_PG_PASSWORD=replace-with-strong-password
|
|
47
|
-
|
|
48
|
-
# Wattswarm runtime and network
|
|
49
|
-
WATTSWARM_P2P_ENABLED=true
|
|
50
|
-
WATTSWARM_P2P_MDNS=true
|
|
51
|
-
WATTSWARM_P2P_PORT=4001
|
|
52
|
-
WATTSWARM_WORKER_CONCURRENCY=16
|
|
53
|
-
WATTSWARM_WORKER_POLL_MS=250
|
|
54
|
-
WATTSWARM_WORKER_LEASE_MS=30000
|
|
55
|
-
WATTSWARM_UDP_ANNOUNCE_ENABLED=false
|
|
56
|
-
WATTSWARM_UDP_ANNOUNCE_MODE=multicast
|
|
57
|
-
WATTSWARM_UDP_ANNOUNCE_ADDR=239.255.42.99
|
|
58
|
-
WATTSWARM_UDP_ANNOUNCE_PORT=37931
|