mycomesh-consumer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # mycomesh-consumer
2
2
 
3
- Node.js CLI for consumers calling a MycoMesh OpenAI-compatible Consumer edge.
4
- The commands remain stateless: wallet funding and V5 Session activation happen
5
- in the browser, while this package sends API requests to the local loopback
6
- edge or an explicitly configured HTTPS Gateway.
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
- - A MycoMesh Consumer base URL (the default is the local loopback edge)
15
- - A consumer API key for authenticated endpoints
16
- - For paid V5 inference, an active Session ID opened by the same wallet in the
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 health
28
+ mycomesh-consumer
30
29
  ```
31
30
 
32
- This installs only the stateless Consumer CLI. It does not install or start
33
- Bridge, Relay, Proxy, or Codex services. Provider operators should install the
34
- separate `mycomesh-provider` package.
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
- For a one-shot command without a global install:
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
- For the local network, start `make consumer-up`, open the local Playground,
62
- connect a browser wallet, fund V5 escrow and approve the one-time `openSession`
63
- transaction. Copy the active Session ID from the local status/Playground, then
64
- set:
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. The CLI does not connect a wallet, move funds,
74
- or open a V5 Session; the browser wallet flow does that once. A public Gateway
75
- can still be used explicitly with `--base-url`, but it is never the default.
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 required
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. A bare OpenAI-shaped request without either value is useful
98
- only with a Gateway whose operator has explicitly configured another billing
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 { main } from "../src/cli.mjs";
3
+ import { isApiInvocation } from "../src/consumer.mjs";
4
4
 
5
- process.exitCode = await main(process.argv.slice(2));
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 { main } from "../src/cli.mjs";
3
+ import { isApiInvocation } from "../src/consumer.mjs";
4
4
 
5
- process.exitCode = await main(process.argv.slice(2));
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.0",
4
- "description": "Stateless MycoMesh Consumer CLI for the local loopback edge or an explicit HTTPS Gateway.",
3
+ "version": "0.1.2",
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,190 @@
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
+ if ! codex_env="$("$DOCKER" compose --project-name "$PROJECT_NAME" --file "$COMPOSE_FILE" exec -T consumer python -m gateway.local_consumer codex-env)"; then
173
+ die "could not load Codex credentials from the local Consumer"
174
+ fi
175
+ eval "$codex_env"
176
+ unset codex_env
177
+ if ! command -v "$CODEX_COMMAND" >/dev/null 2>&1; then
178
+ printf "error: Consumer is ready, but '%s' is not installed on this host\n" "$CODEX_COMMAND" >&2
179
+ exit 127
180
+ fi
181
+
182
+ printf '%s\n' "Opening Codex through the local MycoMesh Consumer."
183
+ exec "$CODEX_COMMAND" \
184
+ -c 'model="gpt-5.5"' \
185
+ -c 'model_provider="mycomesh"' \
186
+ -c 'model_providers.mycomesh.name="MycoMesh"' \
187
+ -c 'model_providers.mycomesh.base_url="http://127.0.0.1:8110/v1"' \
188
+ -c 'model_providers.mycomesh.env_key="MYCOMESH_API_KEY"' \
189
+ -c 'model_providers.mycomesh.wire_api="responses"' \
190
+ "${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.0";
4
+ const CLI_VERSION = "0.1.2";
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.
@@ -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.2";
6
+ const DEFAULT_NODE_IMAGE =
7
+ "ghcr.io/charleslzp/mycomesh-node@sha256:341fff4ee5d1d820d2149c5b1fc4de619ef0abbb41c83408ad8a9f8bfbf31d10";
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 };