mycomesh-consumer 0.1.0 → 0.1.1
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 +36 -24
- package/bin/mycomesh-consumer.mjs +7 -2
- package/bin/mycomesh.mjs +7 -2
- package/consumer.compose.yml +83 -0
- package/package.json +8 -2
- package/scripts/start-consumer.sh +186 -0
- package/src/cli.mjs +1 -1
- package/src/consumer.mjs +208 -0
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# mycomesh-consumer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Local-first launcher and request CLI for the MycoMesh OpenAI-compatible
|
|
4
|
+
Consumer edge. With no arguments it starts the pinned Docker runtime, opens the
|
|
5
|
+
wallet and funding page, waits for a V5 Session, and opens Codex through the
|
|
6
|
+
loopback proxy.
|
|
7
7
|
|
|
8
8
|
The package is published as `mycomesh-consumer`. `mycomesh-consumer` and the
|
|
9
9
|
short `mycomesh` command are equivalent.
|
|
@@ -11,10 +11,9 @@ short `mycomesh` command are equivalent.
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
13
13
|
- Node.js 20 or newer
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
local Web dApp
|
|
14
|
+
- Docker Desktop/Engine with Compose V2
|
|
15
|
+
- The official Codex dependency installed with this npm package
|
|
16
|
+
- An injected browser wallet on Sepolia with enough ETH for transaction gas
|
|
18
17
|
|
|
19
18
|
For local development, install it directly from the repository:
|
|
20
19
|
|
|
@@ -26,14 +25,27 @@ For an npm install from the public registry:
|
|
|
26
25
|
|
|
27
26
|
```sh
|
|
28
27
|
npm install --global mycomesh-consumer
|
|
29
|
-
mycomesh-consumer
|
|
28
|
+
mycomesh-consumer
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
No checkout, separate Codex install, host Python, GNU Make, public Gateway URL, or command arguments are
|
|
32
|
+
required. The command always prints the local onboarding URL and opens it when
|
|
33
|
+
possible. Connect the wallet, mint/deposit test tUSDC, and approve the one-time
|
|
34
|
+
V5 `openSession`; Codex opens after `/ready` confirms the Session on-chain.
|
|
35
|
+
Wallet keys stay in the browser wallet. Consumer credentials, transport
|
|
36
|
+
identity, and Session state stay in a protected Docker volume.
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
The runtime image is pinned by digest. Stop it without deleting state with:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
mycomesh-consumer --stop
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Use `--no-browser` on a headless machine or `--no-codex` to leave only the
|
|
45
|
+
local proxy running. Standard proxy variables are forwarded into the Consumer;
|
|
46
|
+
loopback proxy hosts are translated to `host.docker.internal`.
|
|
47
|
+
|
|
48
|
+
The existing stateless API commands remain available. For example:
|
|
37
49
|
|
|
38
50
|
```sh
|
|
39
51
|
npx --yes --package=mycomesh-consumer mycomesh-consumer health
|
|
@@ -58,10 +70,10 @@ Prefer `MYCOMESH_API_KEY` over `--api-key`: command-line arguments may be stored
|
|
|
58
70
|
in shell history or visible to other local processes. The CLI never writes the
|
|
59
71
|
key to disk and redacts it from HTTP error output.
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
The zero-argument launcher configures Codex directly with a temporary
|
|
74
|
+
`mycomesh` model provider and does not modify `~/.codex/config.toml`. For manual
|
|
75
|
+
API commands, read the local credentials with the repository operator command
|
|
76
|
+
or provide explicit values:
|
|
65
77
|
|
|
66
78
|
```sh
|
|
67
79
|
export MYCOMESH_BASE_URL=http://127.0.0.1:8110/v1
|
|
@@ -70,9 +82,10 @@ export MYCOMESH_SESSION_ID='0x...replace-with-active-v5-session-id'
|
|
|
70
82
|
```
|
|
71
83
|
|
|
72
84
|
The Session ID is not a replacement for the API key. Both values must belong to
|
|
73
|
-
the same local Consumer wallet.
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
the same local Consumer wallet. A standard OpenAI client may omit the
|
|
86
|
+
`mycomesh_session` extension when it calls the local proxy: the proxy selects
|
|
87
|
+
the latest verified active Session. A public Gateway can still be used
|
|
88
|
+
explicitly with `--base-url`, but it is never the default.
|
|
76
89
|
|
|
77
90
|
## Commands
|
|
78
91
|
|
|
@@ -92,11 +105,10 @@ mycomesh chat \
|
|
|
92
105
|
--max-completion-tokens 500
|
|
93
106
|
```
|
|
94
107
|
|
|
95
|
-
The CLI validates `MYCOMESH_SESSION_ID` and adds the
|
|
108
|
+
The request CLI validates `MYCOMESH_SESSION_ID` and adds the
|
|
96
109
|
`mycomesh_session` object automatically. Use `--session-id 0x...` to override it
|
|
97
|
-
for one command.
|
|
98
|
-
|
|
99
|
-
path.
|
|
110
|
+
for one command. The local proxy also accepts a standard OpenAI-shaped request
|
|
111
|
+
without the extension and binds its latest verified Session.
|
|
100
112
|
|
|
101
113
|
Use `--stream` to request SSE. SSE bytes are forwarded to stdout as they arrive,
|
|
102
114
|
without rewriting event boundaries:
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { isApiInvocation } from "../src/consumer.mjs";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const argv = process.argv.slice(2);
|
|
6
|
+
const { main } = isApiInvocation(argv)
|
|
7
|
+
? await import("../src/cli.mjs")
|
|
8
|
+
: await import("../src/consumer.mjs");
|
|
9
|
+
|
|
10
|
+
process.exitCode = await main(argv);
|
package/bin/mycomesh.mjs
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { isApiInvocation } from "../src/consumer.mjs";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
const argv = process.argv.slice(2);
|
|
6
|
+
const { main } = isApiInvocation(argv)
|
|
7
|
+
? await import("../src/cli.mjs")
|
|
8
|
+
: await import("../src/consumer.mjs");
|
|
9
|
+
|
|
10
|
+
process.exitCode = await main(argv);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
services:
|
|
2
|
+
consumer-volume-init:
|
|
3
|
+
image: ${MYCOMESH_NODE_IMAGE:?MYCOMESH_NODE_IMAGE is required}
|
|
4
|
+
user: "0:0"
|
|
5
|
+
network_mode: none
|
|
6
|
+
restart: "no"
|
|
7
|
+
read_only: true
|
|
8
|
+
security_opt:
|
|
9
|
+
- no-new-privileges:true
|
|
10
|
+
cap_drop:
|
|
11
|
+
- ALL
|
|
12
|
+
cap_add:
|
|
13
|
+
- CHOWN
|
|
14
|
+
- DAC_OVERRIDE
|
|
15
|
+
volumes:
|
|
16
|
+
- mycomesh-consumer-data:/volumes/consumer
|
|
17
|
+
entrypoint: ["/bin/sh", "-ec", "chown -R 10001:10001 /volumes/consumer"]
|
|
18
|
+
|
|
19
|
+
consumer:
|
|
20
|
+
image: ${MYCOMESH_NODE_IMAGE:?MYCOMESH_NODE_IMAGE is required}
|
|
21
|
+
user: "10001:10001"
|
|
22
|
+
restart: unless-stopped
|
|
23
|
+
pids_limit: 128
|
|
24
|
+
mem_limit: 256m
|
|
25
|
+
cpus: "1.0"
|
|
26
|
+
logging:
|
|
27
|
+
driver: json-file
|
|
28
|
+
options:
|
|
29
|
+
max-size: 10m
|
|
30
|
+
max-file: "3"
|
|
31
|
+
init: true
|
|
32
|
+
read_only: true
|
|
33
|
+
security_opt:
|
|
34
|
+
- no-new-privileges:true
|
|
35
|
+
cap_drop:
|
|
36
|
+
- ALL
|
|
37
|
+
working_dir: /data
|
|
38
|
+
environment:
|
|
39
|
+
MYCOMESH_CONSUMER_DATA_DIR: /data
|
|
40
|
+
MYCOMESH_CONSUMER_NETWORK_CONFIG: /app/deployments/sepolia-provider-network.json
|
|
41
|
+
MYCOMESH_CONSUMER_PUBLIC_BASE_URL: http://127.0.0.1:8110/v1
|
|
42
|
+
MYCOMESH_CONSUMER_WEB_DIR: /app/web
|
|
43
|
+
MYCOMESH_CONSUMER_DISCOVERY_URLS: ${MYCOMESH_CONSUMER_DISCOVERY_URLS:-}
|
|
44
|
+
MYCOMESH_CONSUMER_SESSION_LIFETIME_SECONDS: ${MYCOMESH_CONSUMER_SESSION_LIFETIME_SECONDS:-86400}
|
|
45
|
+
MYCOMESH_CONSUMER_SESSION_MAX_AMOUNT_UNITS: ${MYCOMESH_CONSUMER_SESSION_MAX_AMOUNT_UNITS:-10000000}
|
|
46
|
+
MYCOMESH_CONSUMER_REQUEST_TIMEOUT_SECONDS: ${MYCOMESH_CONSUMER_REQUEST_TIMEOUT_SECONDS:-300}
|
|
47
|
+
MYCOMESH_CONSUMER_MAX_REQUEST_BYTES: ${MYCOMESH_CONSUMER_MAX_REQUEST_BYTES:-1048576}
|
|
48
|
+
MYCOMESH_CONSUMER_REQUEST_BODY_TIMEOUT_SECONDS: ${MYCOMESH_CONSUMER_REQUEST_BODY_TIMEOUT_SECONDS:-30}
|
|
49
|
+
HTTP_PROXY: ${MYCOMESH_CONSUMER_HTTP_PROXY:-}
|
|
50
|
+
HTTPS_PROXY: ${MYCOMESH_CONSUMER_HTTPS_PROXY:-}
|
|
51
|
+
ALL_PROXY: ${MYCOMESH_CONSUMER_ALL_PROXY:-}
|
|
52
|
+
NO_PROXY: ${MYCOMESH_CONSUMER_NO_PROXY:-127.0.0.1,localhost,::1}
|
|
53
|
+
http_proxy: ${MYCOMESH_CONSUMER_HTTP_PROXY:-}
|
|
54
|
+
https_proxy: ${MYCOMESH_CONSUMER_HTTPS_PROXY:-}
|
|
55
|
+
all_proxy: ${MYCOMESH_CONSUMER_ALL_PROXY:-}
|
|
56
|
+
no_proxy: ${MYCOMESH_CONSUMER_NO_PROXY:-127.0.0.1,localhost,::1}
|
|
57
|
+
extra_hosts:
|
|
58
|
+
- host.docker.internal:host-gateway
|
|
59
|
+
ports:
|
|
60
|
+
- "127.0.0.1:8110:8110"
|
|
61
|
+
volumes:
|
|
62
|
+
- mycomesh-consumer-data:/data
|
|
63
|
+
tmpfs:
|
|
64
|
+
- /tmp:size=32m,mode=1777
|
|
65
|
+
depends_on:
|
|
66
|
+
consumer-volume-init:
|
|
67
|
+
condition: service_completed_successfully
|
|
68
|
+
stop_grace_period: 15s
|
|
69
|
+
healthcheck:
|
|
70
|
+
test:
|
|
71
|
+
- CMD
|
|
72
|
+
- python
|
|
73
|
+
- -c
|
|
74
|
+
- "import json, urllib.request; value=json.load(urllib.request.urlopen('http://127.0.0.1:8110/health', timeout=3)); assert value.get('ok') is True; assert value.get('gateway_dependency') is False; assert value.get('browser_app_ready') is True"
|
|
75
|
+
interval: 15s
|
|
76
|
+
timeout: 5s
|
|
77
|
+
retries: 4
|
|
78
|
+
start_period: 10s
|
|
79
|
+
entrypoint: ["python", "-m", "gateway.local_consumer"]
|
|
80
|
+
command: ["serve", "--host", "0.0.0.0", "--port", "8110"]
|
|
81
|
+
|
|
82
|
+
volumes:
|
|
83
|
+
mycomesh-consumer-data:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mycomesh-consumer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Local-first MycoMesh Consumer launcher and OpenAI-compatible request CLI.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -14,11 +14,17 @@
|
|
|
14
14
|
"bin/mycomesh.mjs",
|
|
15
15
|
"bin/mycomesh-consumer.mjs",
|
|
16
16
|
"src/cli.mjs",
|
|
17
|
+
"src/consumer.mjs",
|
|
18
|
+
"scripts/start-consumer.sh",
|
|
19
|
+
"consumer.compose.yml",
|
|
17
20
|
"README.md"
|
|
18
21
|
],
|
|
19
22
|
"engines": {
|
|
20
23
|
"node": ">=20"
|
|
21
24
|
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@openai/codex": "0.146.0"
|
|
27
|
+
},
|
|
22
28
|
"scripts": {
|
|
23
29
|
"test": "node --test"
|
|
24
30
|
},
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -Eeuo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
5
|
+
COMPOSE_FILE="$SCRIPT_DIR/../consumer.compose.yml"
|
|
6
|
+
PROJECT_NAME="mycomesh-consumer"
|
|
7
|
+
NODE_IMAGE="${MYCOMESH_NODE_IMAGE:-}"
|
|
8
|
+
CODEX_COMMAND="${MYCOMESH_CODEX_COMMAND:-codex}"
|
|
9
|
+
READY_TIMEOUT="${MYCOMESH_CONSUMER_READY_TIMEOUT_SECONDS:-1800}"
|
|
10
|
+
NO_BROWSER=0
|
|
11
|
+
NO_CODEX=0
|
|
12
|
+
STOP=0
|
|
13
|
+
DRY_RUN=0
|
|
14
|
+
CODEX_ARGS=()
|
|
15
|
+
|
|
16
|
+
die() {
|
|
17
|
+
printf 'error: %s\n' "$*" >&2
|
|
18
|
+
exit 64
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
is_docker_cli() {
|
|
22
|
+
local candidate="${1-}" version_output
|
|
23
|
+
[[ -n "$candidate" && -x "$candidate" && ! -d "$candidate" ]] || return 1
|
|
24
|
+
case "$candidate" in */node_modules/*) return 1 ;; esac
|
|
25
|
+
version_output="$("$candidate" --version 2>/dev/null)" || return 1
|
|
26
|
+
[[ "$version_output" == "Docker version "* ]] || return 1
|
|
27
|
+
"$candidate" compose version >/dev/null 2>&1 || return 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
find_docker_cli() {
|
|
31
|
+
local configured="${MYCOMESH_DOCKER_CLI:-}" path_entry candidate name fallback old_ifs="$IFS"
|
|
32
|
+
if [[ -n "$configured" ]]; then
|
|
33
|
+
candidate="$configured"
|
|
34
|
+
[[ "$candidate" == */* ]] || candidate="$(command -v "$candidate" 2>/dev/null || true)"
|
|
35
|
+
is_docker_cli "$candidate" || die "MYCOMESH_DOCKER_CLI is not a Docker CLI with Compose V2"
|
|
36
|
+
printf '%s' "$candidate"
|
|
37
|
+
return
|
|
38
|
+
fi
|
|
39
|
+
IFS=:
|
|
40
|
+
for path_entry in ${PATH:-}; do
|
|
41
|
+
[[ -n "$path_entry" ]] || path_entry=.
|
|
42
|
+
for name in docker docker.exe; do
|
|
43
|
+
candidate="$path_entry/$name"
|
|
44
|
+
if is_docker_cli "$candidate"; then
|
|
45
|
+
IFS="$old_ifs"
|
|
46
|
+
printf '%s' "$candidate"
|
|
47
|
+
return
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
done
|
|
51
|
+
IFS="$old_ifs"
|
|
52
|
+
for fallback in /usr/local/bin/docker /opt/homebrew/bin/docker /usr/bin/docker \
|
|
53
|
+
/Applications/Docker.app/Contents/Resources/bin/docker \
|
|
54
|
+
'/c/Program Files/Docker/Docker/resources/bin/docker.exe'; do
|
|
55
|
+
if is_docker_cli "$fallback"; then
|
|
56
|
+
printf '%s' "$fallback"
|
|
57
|
+
return
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
die "Docker Desktop/Engine with Compose V2 is required"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
rewrite_loopback_proxy() {
|
|
64
|
+
local value="${1-}" scheme authority suffix userinfo host_port tail=""
|
|
65
|
+
case "$value" in *$'\n'*|*$'\r'*) die "Consumer proxy URLs must be single-line values" ;; esac
|
|
66
|
+
if [[ ! "$value" =~ ^([A-Za-z][A-Za-z0-9+.-]*://)([^/?#]*)(.*)$ ]]; then
|
|
67
|
+
printf '%s' "$value"
|
|
68
|
+
return
|
|
69
|
+
fi
|
|
70
|
+
scheme="${BASH_REMATCH[1]}"; authority="${BASH_REMATCH[2]}"; suffix="${BASH_REMATCH[3]}"
|
|
71
|
+
userinfo=""; host_port="$authority"
|
|
72
|
+
if [[ "$authority" == *@* ]]; then userinfo="${authority%@*}@"; host_port="${authority##*@}"; fi
|
|
73
|
+
if [[ "$host_port" == "127.0.0.1" ]]; then :
|
|
74
|
+
elif [[ "$host_port" == 127.0.0.1:* ]]; then tail="${host_port#127.0.0.1}"
|
|
75
|
+
elif [[ "$host_port" == "[::1]" ]]; then :
|
|
76
|
+
elif [[ "$host_port" == "[::1]:"* ]]; then tail="${host_port#\[::1\]}"
|
|
77
|
+
elif [[ "$host_port" =~ ^[Ll][Oo][Cc][Aa][Ll][Hh][Oo][Ss][Tt](:.*)?$ ]]; then tail="${BASH_REMATCH[1]}"
|
|
78
|
+
else printf '%s' "$value"; return
|
|
79
|
+
fi
|
|
80
|
+
printf '%s' "${scheme}${userinfo}host.docker.internal${tail}${suffix}"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
prepare_proxy_env() {
|
|
84
|
+
local all_proxy_value no_proxy_value
|
|
85
|
+
all_proxy_value="${MYCOMESH_CONSUMER_ALL_PROXY:-${all_proxy:-${ALL_PROXY:-}}}"
|
|
86
|
+
export MYCOMESH_CONSUMER_HTTP_PROXY="$(rewrite_loopback_proxy "${MYCOMESH_CONSUMER_HTTP_PROXY:-${http_proxy:-${HTTP_PROXY:-$all_proxy_value}}}")"
|
|
87
|
+
export MYCOMESH_CONSUMER_HTTPS_PROXY="$(rewrite_loopback_proxy "${MYCOMESH_CONSUMER_HTTPS_PROXY:-${https_proxy:-${HTTPS_PROXY:-$all_proxy_value}}}")"
|
|
88
|
+
export MYCOMESH_CONSUMER_ALL_PROXY="$(rewrite_loopback_proxy "$all_proxy_value")"
|
|
89
|
+
no_proxy_value="${MYCOMESH_CONSUMER_NO_PROXY:-${no_proxy:-${NO_PROXY:-}}}"
|
|
90
|
+
export MYCOMESH_CONSUMER_NO_PROXY="${no_proxy_value:+${no_proxy_value},}127.0.0.1,localhost,::1"
|
|
91
|
+
export NO_PROXY="$MYCOMESH_CONSUMER_NO_PROXY"
|
|
92
|
+
export no_proxy="$MYCOMESH_CONSUMER_NO_PROXY"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
run() {
|
|
96
|
+
if ((DRY_RUN)); then printf '+'; printf ' %q' "$@"; printf '\n'; else "$@"; fi
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
compose() {
|
|
100
|
+
run "$DOCKER" compose --project-name "$PROJECT_NAME" --file "$COMPOSE_FILE" "$@"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
open_browser() {
|
|
104
|
+
local url="$1" opener=""
|
|
105
|
+
printf 'MycoMesh Consumer onboarding: %s\n' "$url"
|
|
106
|
+
((NO_BROWSER)) && return
|
|
107
|
+
case "$(uname -s 2>/dev/null || printf unknown)" in
|
|
108
|
+
Darwin) opener=open ;;
|
|
109
|
+
Linux*) command -v xdg-open >/dev/null 2>&1 && opener=xdg-open ;;
|
|
110
|
+
MINGW*|MSYS*|CYGWIN*)
|
|
111
|
+
if command -v cmd.exe >/dev/null 2>&1; then cmd.exe /c start "" "$url" >/dev/null 2>&1 & return; fi ;;
|
|
112
|
+
esac
|
|
113
|
+
if [[ -n "$opener" ]]; then "$opener" "$url" >/dev/null 2>&1 &
|
|
114
|
+
else printf '%s\n' "Open this URL in a browser to connect a wallet, fund it, and activate a V5 Session."; fi
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
while (($#)); do
|
|
118
|
+
case "$1" in
|
|
119
|
+
--node-image) (($# >= 2)) || die "--node-image requires a value"; NODE_IMAGE="$2"; shift 2 ;;
|
|
120
|
+
--codex-command) (($# >= 2)) || die "--codex-command requires a value"; CODEX_COMMAND="$2"; shift 2 ;;
|
|
121
|
+
--ready-timeout) (($# >= 2)) || die "--ready-timeout requires a value"; READY_TIMEOUT="$2"; shift 2 ;;
|
|
122
|
+
--no-browser) NO_BROWSER=1; shift ;;
|
|
123
|
+
--no-codex) NO_CODEX=1; shift ;;
|
|
124
|
+
--stop) STOP=1; shift ;;
|
|
125
|
+
--dry-run) DRY_RUN=1; shift ;;
|
|
126
|
+
--) shift; CODEX_ARGS=("$@"); break ;;
|
|
127
|
+
*) die "unknown option: $1" ;;
|
|
128
|
+
esac
|
|
129
|
+
done
|
|
130
|
+
|
|
131
|
+
[[ -f "$COMPOSE_FILE" ]] || die "bundled Consumer Compose file is missing"
|
|
132
|
+
[[ "$NODE_IMAGE" =~ ^[A-Za-z0-9][A-Za-z0-9._/-]*@sha256:[a-f0-9]{64}$ ]] || die "Consumer image must be pinned by digest"
|
|
133
|
+
[[ "$READY_TIMEOUT" =~ ^[1-9][0-9]*$ ]] || die "--ready-timeout must be a positive integer"
|
|
134
|
+
((READY_TIMEOUT <= 86400)) || die "--ready-timeout must not exceed 86400 seconds"
|
|
135
|
+
|
|
136
|
+
DOCKER="$(find_docker_cli)"
|
|
137
|
+
if ((!DRY_RUN)); then "$DOCKER" info >/dev/null 2>&1 || die "Docker Engine/Desktop is not running"; fi
|
|
138
|
+
prepare_proxy_env
|
|
139
|
+
export MYCOMESH_NODE_IMAGE="$NODE_IMAGE"
|
|
140
|
+
|
|
141
|
+
if ((STOP)); then
|
|
142
|
+
compose stop consumer
|
|
143
|
+
printf '%s\n' "MycoMesh Consumer stopped. Its wallet and Session state remain in the Docker volume."
|
|
144
|
+
exit 0
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
compose pull consumer-volume-init consumer
|
|
148
|
+
if ! compose up -d --wait --wait-timeout 90 consumer; then
|
|
149
|
+
printf '%s\n' "Consumer startup failed. Check whether another process owns 127.0.0.1:8110." >&2
|
|
150
|
+
"$DOCKER" compose --project-name "$PROJECT_NAME" --file "$COMPOSE_FILE" logs --tail=120 consumer >&2 || true
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
url="http://127.0.0.1:8110/app/playground"
|
|
154
|
+
open_browser "$url"
|
|
155
|
+
|
|
156
|
+
if ((DRY_RUN || NO_CODEX)); then
|
|
157
|
+
printf '%s\n' "MycoMesh Consumer is running at http://127.0.0.1:8110/v1"
|
|
158
|
+
((DRY_RUN)) && printf '%s\n' "Dry run complete; no container or Codex process was started."
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
printf '%s\n' "Waiting for the browser to activate the local V5 Session..."
|
|
163
|
+
command -v curl >/dev/null 2>&1 || die "curl is required while waiting for wallet onboarding"
|
|
164
|
+
started_at="$(date +%s)"
|
|
165
|
+
while ! curl --noproxy '*' --fail --silent --max-time 3 http://127.0.0.1:8110/ready >/dev/null 2>&1; do
|
|
166
|
+
if (( $(date +%s) - started_at >= READY_TIMEOUT )); then
|
|
167
|
+
die "timed out waiting for the local Consumer V5 Session; reopen $url"
|
|
168
|
+
fi
|
|
169
|
+
sleep 2
|
|
170
|
+
done
|
|
171
|
+
|
|
172
|
+
eval "$("$DOCKER" compose --project-name "$PROJECT_NAME" --file "$COMPOSE_FILE" exec -T consumer python -m gateway.local_consumer codex-env)"
|
|
173
|
+
if ! command -v "$CODEX_COMMAND" >/dev/null 2>&1; then
|
|
174
|
+
printf "error: Consumer is ready, but '%s' is not installed on this host\n" "$CODEX_COMMAND" >&2
|
|
175
|
+
exit 127
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
printf '%s\n' "Opening Codex through the local MycoMesh Consumer."
|
|
179
|
+
exec "$CODEX_COMMAND" \
|
|
180
|
+
-c 'model="mycomesh-codex-standard-v1"' \
|
|
181
|
+
-c 'model_provider="mycomesh"' \
|
|
182
|
+
-c 'model_providers.mycomesh.name="MycoMesh"' \
|
|
183
|
+
-c 'model_providers.mycomesh.base_url="http://127.0.0.1:8110/v1"' \
|
|
184
|
+
-c 'model_providers.mycomesh.env_key="MYCOMESH_API_KEY"' \
|
|
185
|
+
-c 'model_providers.mycomesh.wire_api="responses"' \
|
|
186
|
+
"${CODEX_ARGS[@]}"
|
package/src/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { once } from "node:events";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
const CLI_VERSION = "0.1.
|
|
4
|
+
const CLI_VERSION = "0.1.1";
|
|
5
5
|
// The npm client is local-first. A public Gateway is an explicit override;
|
|
6
6
|
// the default remains the loopback Consumer edge so a blocked domain cannot
|
|
7
7
|
// strand the Codex client.
|
package/src/consumer.mjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const CONSUMER_RELEASE_VERSION = "0.1.1";
|
|
6
|
+
const DEFAULT_NODE_IMAGE =
|
|
7
|
+
"ghcr.io/charleslzp/mycomesh-node@sha256:aeb61a318a29b70a96f54daacae6fc35ca7cad719b9de295d76306a5cb2b6222";
|
|
8
|
+
const API_COMMANDS = new Set(["health", "models", "responses", "chat"]);
|
|
9
|
+
const API_VALUE_OPTIONS = new Set([
|
|
10
|
+
"--base-url",
|
|
11
|
+
"--api-key",
|
|
12
|
+
"--timeout",
|
|
13
|
+
"--session-id",
|
|
14
|
+
"--json",
|
|
15
|
+
"--model",
|
|
16
|
+
"--input",
|
|
17
|
+
"--message",
|
|
18
|
+
"--system",
|
|
19
|
+
"--max-output-tokens",
|
|
20
|
+
"--max-tokens",
|
|
21
|
+
"--max-completion-tokens",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const HELP = `Usage: mycomesh-consumer [options] [-- codex-options]
|
|
25
|
+
|
|
26
|
+
Start the local MycoMesh Consumer. No options are needed.
|
|
27
|
+
|
|
28
|
+
The default command starts the pinned Docker runtime, opens the local wallet
|
|
29
|
+
and funding page, waits for an activated V5 Session, then opens Codex through
|
|
30
|
+
the loopback Consumer at http://127.0.0.1:8110/v1.
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--no-browser Print the onboarding URL without opening it
|
|
34
|
+
--no-codex Start the Consumer and browser without opening Codex
|
|
35
|
+
--stop Stop the Consumer without deleting its wallet state
|
|
36
|
+
--codex-command PATH Codex executable (default: codex)
|
|
37
|
+
--ready-timeout SEC Wallet onboarding timeout (default: 1800)
|
|
38
|
+
--node-image IMAGE Advanced: override the pinned Consumer image
|
|
39
|
+
--dry-run Print the planned Docker operations
|
|
40
|
+
-h, --help Show this help
|
|
41
|
+
-v, --version Show the package version
|
|
42
|
+
|
|
43
|
+
Existing API commands remain available:
|
|
44
|
+
mycomesh-consumer health
|
|
45
|
+
mycomesh-consumer models
|
|
46
|
+
mycomesh-consumer responses "hello"
|
|
47
|
+
mycomesh-consumer chat "hello"
|
|
48
|
+
|
|
49
|
+
Docker Desktop/Engine is required. The package installs official Codex and
|
|
50
|
+
wallet keys remain in the browser wallet; local Consumer credentials and
|
|
51
|
+
Sessions remain in a protected Docker volume.`;
|
|
52
|
+
|
|
53
|
+
class ConsumerCliError extends Error {
|
|
54
|
+
constructor(message, exitCode = 1) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.name = "ConsumerCliError";
|
|
57
|
+
this.exitCode = exitCode;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function main(argv, dependencies = {}) {
|
|
62
|
+
const env = dependencies.env ?? process.env;
|
|
63
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
64
|
+
const stderr = dependencies.stderr ?? process.stderr;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = parseArguments(argv, env);
|
|
67
|
+
if (parsed.help) {
|
|
68
|
+
stdout.write(`${HELP}\n`);
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (parsed.version) {
|
|
72
|
+
stdout.write(`${CONSUMER_RELEASE_VERSION}\n`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
const script = dependencies.scriptPath
|
|
76
|
+
?? fileURLToPath(new URL("../scripts/start-consumer.sh", import.meta.url));
|
|
77
|
+
return await runScript(script, toScriptArgs(parsed), {
|
|
78
|
+
env,
|
|
79
|
+
spawn: dependencies.spawn ?? defaultSpawn,
|
|
80
|
+
});
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const exitCode = error instanceof ConsumerCliError ? error.exitCode : 1;
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
stderr.write(`mycomesh consumer: ${message}\n`);
|
|
85
|
+
return exitCode;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function parseArguments(argv, env = process.env) {
|
|
90
|
+
const parsed = {
|
|
91
|
+
nodeImage: env.MYCOMESH_NODE_IMAGE || DEFAULT_NODE_IMAGE,
|
|
92
|
+
codexCommand: env.MYCOMESH_CODEX_COMMAND || bundledCodexCommand(),
|
|
93
|
+
readyTimeout: env.MYCOMESH_CONSUMER_READY_TIMEOUT_SECONDS || undefined,
|
|
94
|
+
noBrowser: false,
|
|
95
|
+
noCodex: false,
|
|
96
|
+
stop: false,
|
|
97
|
+
dryRun: false,
|
|
98
|
+
help: false,
|
|
99
|
+
version: false,
|
|
100
|
+
codexArgs: [],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
104
|
+
const token = argv[index];
|
|
105
|
+
if (token === "--") {
|
|
106
|
+
parsed.codexArgs = argv.slice(index + 1);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
if (token === "-h" || token === "--help") {
|
|
110
|
+
parsed.help = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (token === "-v" || token === "--version") {
|
|
114
|
+
parsed.version = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (token === "--no-browser") {
|
|
118
|
+
parsed.noBrowser = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (token === "--no-codex") {
|
|
122
|
+
parsed.noCodex = true;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (token === "--stop") {
|
|
126
|
+
parsed.stop = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (token === "--dry-run") {
|
|
130
|
+
parsed.dryRun = true;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const separator = token.indexOf("=");
|
|
134
|
+
const name = separator === -1 ? token : token.slice(0, separator);
|
|
135
|
+
let value = separator === -1 ? undefined : token.slice(separator + 1);
|
|
136
|
+
if (!["--node-image", "--codex-command", "--ready-timeout"].includes(name)) {
|
|
137
|
+
throw new ConsumerCliError(`unknown option: ${token}`, 2);
|
|
138
|
+
}
|
|
139
|
+
if (value === undefined) {
|
|
140
|
+
index += 1;
|
|
141
|
+
value = argv[index];
|
|
142
|
+
}
|
|
143
|
+
if (!value) throw new ConsumerCliError(`${name} requires a value`, 2);
|
|
144
|
+
if (name === "--node-image") parsed.nodeImage = value;
|
|
145
|
+
if (name === "--codex-command") parsed.codexCommand = value;
|
|
146
|
+
if (name === "--ready-timeout") parsed.readyTimeout = value;
|
|
147
|
+
}
|
|
148
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]*@sha256:[a-f0-9]{64}$/.test(parsed.nodeImage)) {
|
|
149
|
+
throw new ConsumerCliError("Consumer image must be pinned by digest", 2);
|
|
150
|
+
}
|
|
151
|
+
if (parsed.readyTimeout && !/^[1-9][0-9]*$/.test(parsed.readyTimeout)) {
|
|
152
|
+
throw new ConsumerCliError("--ready-timeout must be a positive integer", 2);
|
|
153
|
+
}
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function bundledCodexCommand() {
|
|
158
|
+
try {
|
|
159
|
+
return createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js");
|
|
160
|
+
} catch {
|
|
161
|
+
return "codex";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function isApiInvocation(argv) {
|
|
166
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
167
|
+
const token = argv[index];
|
|
168
|
+
if (token === "--") return false;
|
|
169
|
+
if (token.startsWith("--")) {
|
|
170
|
+
const name = token.split("=", 1)[0];
|
|
171
|
+
if (!token.includes("=") && API_VALUE_OPTIONS.has(name)) index += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (token.startsWith("-")) continue;
|
|
175
|
+
return API_COMMANDS.has(token);
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function toScriptArgs(parsed) {
|
|
181
|
+
const args = ["--node-image", parsed.nodeImage];
|
|
182
|
+
if (parsed.codexCommand) args.push("--codex-command", parsed.codexCommand);
|
|
183
|
+
if (parsed.readyTimeout) args.push("--ready-timeout", parsed.readyTimeout);
|
|
184
|
+
if (parsed.noBrowser) args.push("--no-browser");
|
|
185
|
+
if (parsed.noCodex) args.push("--no-codex");
|
|
186
|
+
if (parsed.stop) args.push("--stop");
|
|
187
|
+
if (parsed.dryRun) args.push("--dry-run");
|
|
188
|
+
if (parsed.codexArgs.length) args.push("--", ...parsed.codexArgs);
|
|
189
|
+
return args;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function runScript(script, args, { env, spawn }) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
let child;
|
|
195
|
+
try {
|
|
196
|
+
child = spawn("bash", [script, ...args], { env: { ...env }, stdio: "inherit" });
|
|
197
|
+
} catch (error) {
|
|
198
|
+
reject(new ConsumerCliError(`could not start bash: ${error.message}`));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
child.once("error", (error) => reject(new ConsumerCliError(`could not start bash: ${error.message}`)));
|
|
202
|
+
child.once("exit", (code, signal) => {
|
|
203
|
+
resolve(typeof code === "number" ? code : 128 + ({ SIGHUP: 1, SIGINT: 2, SIGTERM: 15 }[signal] ?? 1));
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { CONSUMER_RELEASE_VERSION, DEFAULT_NODE_IMAGE, HELP as CONSUMER_HELP };
|