mycomesh-consumer 0.1.34 → 0.1.36

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
@@ -65,6 +65,16 @@ mycomesh responses --input 'hello' --model mycomesh-codex-standard-v1
65
65
 
66
66
  The browser's export block is the canonical way to obtain both values.
67
67
 
68
+ ## Temporary sharing
69
+
70
+ The local page can create one time-limited HTTPS API URL and an independent
71
+ `myco_share_...` key for testing an OpenAI-compatible client outside the
72
+ Consumer machine. Install `cloudflared`, choose a lifetime, and click **Start
73
+ sharing**. The public endpoint exposes only models, Responses, Chat
74
+ Completions, and Codex search routes; it does not expose the dashboard or the
75
+ payment key. Stopping the share, reaching its expiry, or restarting the
76
+ Consumer invalidates the temporary key.
77
+
68
78
  ## Relay scheduling and V8 payments
69
79
 
70
80
  The Consumer checks each configured Relay's V8 health and automatically tries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mycomesh-consumer",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Native Node.js MycoMesh Consumer and OpenAI-compatible request CLI.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { spawn as defaultSpawn } from "node:child_process";
2
3
  import { createServer } from "node:http";
3
4
  import { homedir } from "node:os";
4
5
  import { join, dirname } from "node:path";
@@ -81,6 +82,9 @@ const RESPONSES_REQUEST_OPTION_FIELDS = new Set([
81
82
  "user",
82
83
  ]);
83
84
  const RESPONSES_LOCAL_OPTION_FIELDS = new Set(["stream", "stream_options"]);
85
+ const MAX_SHARE_MINUTES = 24 * 60;
86
+ const TUNNEL_START_TIMEOUT_MS = 30_000;
87
+ const TUNNEL_READY_TIMEOUT_MS = 90_000;
84
88
 
85
89
  const DEFAULT_NETWORK = Object.freeze({
86
90
  chain_id: DEFAULT_CHAIN_ID,
@@ -354,8 +358,11 @@ function normalizeInferenceOptions(endpoint, options) {
354
358
  if (typeof options !== "object" || Array.isArray(options)) {
355
359
  throw new Error("inference request options must be a JSON object");
356
360
  }
361
+ const requestOptions = Object.fromEntries(
362
+ Object.entries(options).filter(([key]) => !RESPONSES_LOCAL_OPTION_FIELDS.has(key)),
363
+ );
357
364
  if (endpoint !== "responses") {
358
- if (Object.keys(options).length) {
365
+ if (Object.keys(requestOptions).length) {
359
366
  throw new Error("inference request options are supported only for responses");
360
367
  }
361
368
  return null;
@@ -365,7 +372,7 @@ function normalizeInferenceOptions(endpoint, options) {
365
372
  if (unknown.length) throw new Error(`unsupported Responses request options: ${unknown.join(", ")}`);
366
373
  const normalized = {};
367
374
  for (const key of [...RESPONSES_REQUEST_OPTION_FIELDS].sort()) {
368
- if (Object.prototype.hasOwnProperty.call(options, key)) normalized[key] = options[key];
375
+ if (Object.prototype.hasOwnProperty.call(requestOptions, key)) normalized[key] = requestOptions[key];
369
376
  }
370
377
  return Object.keys(normalized).length ? normalized : null;
371
378
  }
@@ -587,6 +594,10 @@ export class NativeConsumerState {
587
594
  this.paymentKeyFromEnv = Boolean(String(env.MYCOMESH_V8_PAYMENT_KEY || "").trim());
588
595
  this.paymentKey = this.loadPaymentKey(env.MYCOMESH_V8_PAYMENT_KEY);
589
596
  this.paymentAddress = paymentKeyAddress(this.paymentKey);
597
+ this.tunnelCommand = options.tunnelCommand || env.MYCOMESH_CONSUMER_TUNNEL_COMMAND || "cloudflared";
598
+ this.tunnelSpawn = options.tunnelSpawn || defaultSpawn;
599
+ this.tunnelProbe = options.tunnelProbe || waitForTunnelReady;
600
+ this.share = null;
590
601
  }
591
602
 
592
603
  loadPaymentKey(configured) {
@@ -717,6 +728,7 @@ export class NativeConsumerState {
717
728
  input_tokens: allHistory.reduce((total, item) => total + Number(item.input_tokens || 0), 0),
718
729
  output_tokens: allHistory.reduce((total, item) => total + Number(item.output_tokens || 0), 0),
719
730
  },
731
+ share: this.sharePayload(),
720
732
  };
721
733
  try {
722
734
  const grant = await this.keyGrant(this.paymentAddress);
@@ -740,6 +752,80 @@ export class NativeConsumerState {
740
752
  return payload;
741
753
  }
742
754
 
755
+ sharePayload() {
756
+ const share = this.activeShare();
757
+ return share ? {
758
+ active: true,
759
+ base_url: share.baseUrl,
760
+ api_key: share.apiKey,
761
+ expires_at: share.expiresAt,
762
+ } : { active: false };
763
+ }
764
+
765
+ activeShare() {
766
+ if (this.share && this.share.expiresAt <= Math.floor(Date.now() / 1000)) {
767
+ void this.stopShare();
768
+ return null;
769
+ }
770
+ return this.share;
771
+ }
772
+
773
+ authorizeBearer(authorization, { shareOnly = false } = {}) {
774
+ if (!shareOnly && sameSecret(authorization, `Bearer ${this.paymentKey}`)) return true;
775
+ const share = this.activeShare();
776
+ return Boolean(share && sameSecret(authorization, `Bearer ${share.apiKey}`));
777
+ }
778
+
779
+ async startShare(minutesValue) {
780
+ const minutes = Number(minutesValue);
781
+ if (!Number.isInteger(minutes) || minutes < 1 || minutes > MAX_SHARE_MINUTES) {
782
+ throw new Error(`share duration must be between 1 and ${MAX_SHARE_MINUTES} minutes`);
783
+ }
784
+ await this.stopShare();
785
+ const runtime = createConsumerServer(this, { host: "127.0.0.1", port: 0, publicOnly: true });
786
+ const address = await runtime.listen();
787
+ const apiKey = `myco_share_${randomBytes(24).toString("base64url")}`;
788
+ const share = {
789
+ apiKey,
790
+ baseUrl: "",
791
+ expiresAt: Math.floor(Date.now() / 1000) + minutes * 60,
792
+ process: null,
793
+ runtime,
794
+ timer: null,
795
+ };
796
+ this.share = share;
797
+ try {
798
+ const child = this.tunnelSpawn(
799
+ this.tunnelCommand,
800
+ ["tunnel", "--no-autoupdate", "--url", `http://127.0.0.1:${address.port}`],
801
+ { stdio: ["ignore", "pipe", "pipe"] },
802
+ );
803
+ share.process = child;
804
+ const publicUrl = await waitForTunnelUrl(child);
805
+ if (this.share !== share) throw new Error("temporary share was cancelled");
806
+ await this.tunnelProbe(publicUrl, apiKey, this.dispatcher);
807
+ if (this.share !== share) throw new Error("temporary share was cancelled");
808
+ share.baseUrl = `${publicUrl.replace(/\/+$/, "")}/v1`;
809
+ share.timer = setTimeout(() => { void this.stopShare(); }, minutes * 60 * 1000);
810
+ share.timer.unref?.();
811
+ child.once("exit", () => { if (this.share === share) void this.stopShare(); });
812
+ return this.sharePayload();
813
+ } catch (error) {
814
+ await this.stopShare();
815
+ throw error;
816
+ }
817
+ }
818
+
819
+ async stopShare() {
820
+ const share = this.share;
821
+ this.share = null;
822
+ if (!share) return { active: false };
823
+ if (share.timer) clearTimeout(share.timer);
824
+ if (share.process && share.process.exitCode === null) share.process.kill("SIGTERM");
825
+ await share.runtime?.close();
826
+ return { active: false };
827
+ }
828
+
743
829
  pendingPaymentKey() {
744
830
  if (!existsSync(this.pendingKeyPath)) return null;
745
831
  const value = readFileSync(this.pendingKeyPath, "utf8").trim();
@@ -949,10 +1035,59 @@ function writeJson(response, status, value, headers = {}) {
949
1035
  response.end(body);
950
1036
  }
951
1037
 
952
- async function handleInference(state, request, response, path, alphaSearchQuery) {
1038
+ function waitForTunnelUrl(child) {
1039
+ return new Promise((resolve, reject) => {
1040
+ let output = "";
1041
+ const finish = (error, url) => {
1042
+ clearTimeout(timer);
1043
+ child.removeListener("error", onError);
1044
+ child.removeListener("exit", onExit);
1045
+ child.stdout?.removeListener("data", onData);
1046
+ child.stderr?.removeListener("data", onData);
1047
+ if (error) reject(error); else resolve(url);
1048
+ };
1049
+ const onData = (chunk) => {
1050
+ output = `${output}${String(chunk)}`.slice(-16_384);
1051
+ const match = output.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
1052
+ if (match) finish(null, match[0]);
1053
+ };
1054
+ const onError = (error) => finish(new Error(`could not start cloudflared: ${error.message}`));
1055
+ const onExit = (code) => finish(new Error(`cloudflared exited before publishing a URL (${code ?? "signal"})`));
1056
+ const timer = setTimeout(
1057
+ () => finish(new Error("timed out waiting for the temporary HTTPS URL")),
1058
+ TUNNEL_START_TIMEOUT_MS,
1059
+ );
1060
+ child.once("error", onError);
1061
+ child.once("exit", onExit);
1062
+ child.stdout?.on("data", onData);
1063
+ child.stderr?.on("data", onData);
1064
+ });
1065
+ }
1066
+
1067
+ async function waitForTunnelReady(publicUrl, apiKey, dispatcher) {
1068
+ const deadline = Date.now() + TUNNEL_READY_TIMEOUT_MS;
1069
+ let lastError;
1070
+ while (Date.now() < deadline) {
1071
+ try {
1072
+ const response = await fetchWithTimeout(`${publicUrl.replace(/\/+$/, "")}/v1/health`, {
1073
+ headers: { authorization: `Bearer ${apiKey}` },
1074
+ dispatcher,
1075
+ }, Math.min(3000, deadline - Date.now()));
1076
+ const payload = await readJsonResponse(response);
1077
+ if (response.ok && payload.protocol === "mycomesh-temporary-share/v1") return;
1078
+ lastError = new Error(`HTTP ${response.status}`);
1079
+ } catch (error) {
1080
+ lastError = error;
1081
+ }
1082
+ await new Promise((resolve) => setTimeout(resolve, 500));
1083
+ }
1084
+ throw new Error(`temporary HTTPS URL was not reachable: ${lastError?.message || "timed out"}`);
1085
+ }
1086
+
1087
+ async function handleInference(state, request, response, path, alphaSearchQuery, shareOnly = false) {
953
1088
  const authorization = String(request.headers.authorization || "");
954
- if (!sameSecret(authorization, `Bearer ${state.paymentKey}`)) {
955
- writeJson(response, 401, openaiError("invalid MycoMesh payment key", "invalid_api_key"));
1089
+ if (!state.authorizeBearer(authorization, { shareOnly })) {
1090
+ writeJson(response, 401, openaiError("invalid MycoMesh access key", "invalid_api_key"));
956
1091
  return;
957
1092
  }
958
1093
  let body;
@@ -1008,11 +1143,38 @@ async function handleInference(state, request, response, path, alphaSearchQuery)
1008
1143
  }
1009
1144
  }
1010
1145
 
1011
- export function createConsumerServer(state, { host = "127.0.0.1", port = 8110 } = {}) {
1146
+ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110, publicOnly = false } = {}) {
1012
1147
  const server = createServer(async (request, response) => {
1013
1148
  try {
1014
1149
  const url = new URL(request.url || "/", `http://${request.headers.host || "127.0.0.1"}`);
1015
1150
  const path = url.pathname;
1151
+ if (publicOnly) {
1152
+ response.setHeader("Access-Control-Allow-Origin", "*");
1153
+ response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
1154
+ response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1155
+ response.setHeader("Access-Control-Expose-Headers", "PAYMENT-RESPONSE, Retry-After");
1156
+ if (request.method === "OPTIONS") { response.writeHead(204); response.end(); return; }
1157
+ if (request.method === "GET" && (path === "/health" || path === "/v1/health")) {
1158
+ if (!state.authorizeBearer(String(request.headers.authorization || ""), { shareOnly: true })) { writeJson(response, 401, openaiError("invalid temporary access key", "invalid_api_key")); return; }
1159
+ writeJson(response, 200, { ok: true, protocol: "mycomesh-temporary-share/v1", expires_at: state.activeShare()?.expiresAt });
1160
+ return;
1161
+ }
1162
+ if (request.method === "GET" && (path === "/models" || path === "/v1/models" || path === "/backend-api/codex/models")) {
1163
+ if (!state.authorizeBearer(String(request.headers.authorization || ""), { shareOnly: true })) { writeJson(response, 401, openaiError("invalid temporary access key", "invalid_api_key")); return; }
1164
+ try { const selected = await state.chooseRelay(); writeJson(response, 200, { object: "list", data: [{ id: selected.health.v8.model || DEFAULT_MODEL, object: "model", owned_by: "mycomesh", relay: selected.relayUrl }] }); }
1165
+ catch (error) { writeJson(response, 503, openaiError(error.message, "relay_unavailable")); }
1166
+ return;
1167
+ }
1168
+ if (request.method === "POST" && ["/responses", "/v1/responses", "/v1/v1/responses", "/backend-api/codex/responses", "/responses/compact", "/v1/responses/compact", "/v1/v1/responses/compact", "/backend-api/codex/responses/compact", "/chat/completions", "/v1/chat/completions"].includes(path)) {
1169
+ const relayPath = path.endsWith("/chat/completions") ? "/v1/chat/completions" : path.endsWith("/responses/compact") ? "/v1/responses/compact" : "/v1/responses";
1170
+ await handleInference(state, request, response, relayPath, undefined, true); return;
1171
+ }
1172
+ if (request.method === "POST" && ["/alpha/search", "/v1/alpha/search", "/backend-api/codex/alpha/search"].includes(path)) {
1173
+ await handleInference(state, request, response, "/v1/responses", Object.fromEntries(url.searchParams), true); return;
1174
+ }
1175
+ writeJson(response, 404, openaiError("route not found", "invalid_request_error"));
1176
+ return;
1177
+ }
1016
1178
  if (request.method === "GET" && path === "/") {
1017
1179
  response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
1018
1180
  response.end(consumerHtml());
@@ -1049,6 +1211,17 @@ export function createConsumerServer(state, { host = "127.0.0.1", port = 8110 }
1049
1211
  catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
1050
1212
  return;
1051
1213
  }
1214
+ if (request.method === "POST" && path === "/v1/mycomesh/local/share/start") {
1215
+ if (!sameSecret(String(request.headers.authorization || ""), `Bearer ${state.paymentKey}`)) { writeJson(response, 401, { ok: false, error: "invalid local payment key" }); return; }
1216
+ try { const value = await decodeRequestBody(request); writeJson(response, 200, { ok: true, share: await state.startShare(value.minutes) }); }
1217
+ catch (error) { writeJson(response, 400, { ok: false, error: error.message }); }
1218
+ return;
1219
+ }
1220
+ if (request.method === "POST" && path === "/v1/mycomesh/local/share/stop") {
1221
+ if (!sameSecret(String(request.headers.authorization || ""), `Bearer ${state.paymentKey}`)) { writeJson(response, 401, { ok: false, error: "invalid local payment key" }); return; }
1222
+ writeJson(response, 200, { ok: true, share: await state.stopShare() });
1223
+ return;
1224
+ }
1052
1225
  if (request.method === "POST" && ["/responses", "/v1/responses", "/v1/v1/responses", "/backend-api/codex/responses", "/responses/compact", "/v1/responses/compact", "/v1/v1/responses/compact", "/backend-api/codex/responses/compact", "/chat/completions", "/v1/chat/completions"].includes(path)) {
1053
1226
  const relayPath = path.endsWith("/chat/completions")
1054
1227
  ? "/v1/chat/completions"
@@ -1168,21 +1341,23 @@ function consumerHtml() {
1168
1341
  body{font:15px system-ui;max-width:960px;margin:32px auto;padding:0 18px;color:#17211d;background:#f5f7f5}
1169
1342
  h1{margin-bottom:4px}.muted{color:#68736e}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:14px}
1170
1343
  .panel{background:white;border:1px solid #d9e0dc;border-radius:6px;padding:18px;margin:14px 0}.value{word-break:break-all;background:#f1f4f2;padding:10px;border-radius:4px;font:12px monospace;white-space:pre-wrap}
1171
- button{padding:9px 12px;border:1px solid #177b57;border-radius:4px;background:#177b57;color:#fff;cursor:pointer}input{padding:9px;border:1px solid #c7d0ca;border-radius:4px}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
1344
+ button{padding:9px 12px;border:1px solid #177b57;border-radius:4px;background:#177b57;color:#fff;cursor:pointer}input,select{padding:9px;border:1px solid #c7d0ca;border-radius:4px;background:#fff}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
1172
1345
  table{width:100%;border-collapse:collapse}td,th{padding:8px;border-bottom:1px solid #e1e7e3;text-align:left;font-size:13px}
1173
1346
  </style></head><body>
1174
1347
  <h1>MycoMesh Consumer V8</h1><p class="muted">Native Node runtime. This page manages credentials, prepaid balance, payment keys, and local consumption history.</p>
1175
1348
  <div class="grid"><section class="panel"><h2>Credentials</h2><p>API URL</p><div id="url" class="value">Loading...</div><p>Payment key</p><div id="key" class="value"></div><p>Shell export</p><div id="export" class="value"></div><button id="copy" type="button">Copy export</button></section>
1176
1349
  <section class="panel"><h2>Usage</h2><p id="usage">Loading...</p><p id="balance"></p><p>Key address</p><div id="address" class="value"></div><div class="row"><button id="register" type="button">Register key</button><button id="rotate" type="button">Rotate key</button></div></section></div>
1177
1350
  <section class="panel"><h2>Prepaid top-up</h2><div class="row"><button id="connect" type="button">Connect wallet</button><input id="wallet" placeholder="Wallet address"><input id="amount" placeholder="10.00"><button id="topup" type="button">Top up</button></div><pre id="plan" class="value"></pre></section>
1351
+ <section class="panel"><h2>Temporary share</h2><div class="row"><select id="shareMinutes"><option value="10">10 minutes</option><option value="30" selected>30 minutes</option><option value="60">1 hour</option><option value="360">6 hours</option></select><button id="shareStart" type="button">Start sharing</button><button id="shareStop" type="button">Stop</button></div><p>API URL</p><div id="shareUrl" class="value">Inactive</div><p>Temporary key</p><div id="shareKey" class="value"></div><p id="shareExpiry" class="muted"></p><button id="shareCopy" type="button">Copy export</button></section>
1178
1352
  <section class="panel"><h2>Consumption history</h2><table><thead><tr><th>Time</th><th>Model</th><th>Tokens</th><th>Fee</th><th>Relay</th></tr></thead><tbody id="history"></tbody></table></section>
1179
1353
  <script>
1180
1354
  let state;let wallet;const $=id=>document.getElementById(id);
1181
- async function api(path,options={}){const r=await fetch(path,options);const v=await r.json();if(!r.ok)throw Error(v.error||'request failed');return v}
1355
+ async function api(path,options={}){const r=await fetch(path,options);const v=await r.json();if(!r.ok)throw Error(typeof v.error==='string'?v.error:(v.error?.message||'request failed'));return v}
1182
1356
  function render(){
1183
1357
  $('url').textContent=state.credentials.base_url;$('key').textContent=state.credentials.api_key;$('export').textContent=state.credentials.export;$('address').textContent=state.key.address;
1184
1358
  $('usage').textContent=state.usage.request_count+' requests, '+state.usage.total_spent_units+' units';$('balance').textContent='Available balance: '+(state.account?.available_balance_units||'unknown');
1185
1359
  $('history').innerHTML=(state.history||[]).map(x=>'<tr><td>'+new Date(x.timestamp*1000).toLocaleString()+'</td><td>'+x.model+'</td><td>'+x.input_tokens+' / '+x.output_tokens+'</td><td>'+x.actual_fee_units+'</td><td>'+x.relay_url+'</td></tr>').join('')||'<tr><td colspan="5">No consumption recorded.</td></tr>';
1360
+ $('shareUrl').textContent=state.share?.base_url||'Inactive';$('shareKey').textContent=state.share?.api_key||'';$('shareExpiry').textContent=state.share?.expires_at?'Expires '+new Date(state.share.expires_at*1000).toLocaleString():'';
1186
1361
  }
1187
1362
  async function load(){state=await api('/v1/mycomesh/local/dashboard');render()}
1188
1363
  async function connectWallet(){if(!window.ethereum)throw Error('No injected wallet found');const accounts=await window.ethereum.request({method:'eth_requestAccounts'});wallet=accounts[0];if(!wallet)throw Error('Wallet was not connected');$('wallet').value=wallet;$('connect').textContent=wallet.slice(0,6)+'...'+wallet.slice(-4)}
@@ -1194,6 +1369,9 @@ $('copy').onclick=()=>navigator.clipboard?.writeText($('export').textContent);
1194
1369
  $('connect').onclick=()=>connectWallet().catch(error=>$('plan').textContent=error.message);
1195
1370
  $('register').onclick=()=>showPlan('register_key',true);$('topup').onclick=()=>showPlan('top_up',true);
1196
1371
  $('rotate').onclick=async()=>{try{await requireWallet();const oldAddress=state.key.address;const pending=await api('/v1/mycomesh/local/key/prepare',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key}});const plan=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'register_key',wallet})});await sendPlan(plan);let activated;for(let i=0;i<8&&!activated;i++){try{activated=await api('/v1/mycomesh/local/key/activate',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key,'content-type':'application/json'},body:JSON.stringify({wallet})})}catch(error){if(i===7)throw error;await new Promise(resolve=>setTimeout(resolve,1800))}}const revoke=await api('/v1/mycomesh/local/transactions',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'revoke_key',wallet,key_address:oldAddress})});await sendPlan(revoke);$('plan').textContent=JSON.stringify({pending,activated,revoke},null,2);await load()}catch(error){$('plan').textContent=error.message}};
1372
+ $('shareStart').onclick=async()=>{const button=$('shareStart');button.disabled=true;button.textContent='Starting...';try{const v=await api('/v1/mycomesh/local/share/start',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key,'content-type':'application/json'},body:JSON.stringify({minutes:Number($('shareMinutes').value)})});state.share=v.share;render()}catch(error){$('shareExpiry').textContent=error.message}finally{button.disabled=false;button.textContent='Start sharing'}};
1373
+ $('shareStop').onclick=async()=>{try{const v=await api('/v1/mycomesh/local/share/stop',{method:'POST',headers:{authorization:'Bearer '+state.credentials.api_key}});state.share=v.share;render()}catch(error){$('shareExpiry').textContent=error.message}};
1374
+ $('shareCopy').onclick=()=>navigator.clipboard?.writeText("export OPENAI_BASE_URL='"+$('shareUrl').textContent+"'\nexport OPENAI_API_KEY='"+$('shareKey').textContent+"'");
1197
1375
  load().catch(error=>$('plan').textContent=error.message);
1198
1376
  </script></body></html>`;
1199
1377
  }
package/src/consumer.mjs CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  createConsumerServer,
12
12
  } from "./consumer-runtime.mjs";
13
13
 
14
- export const CONSUMER_RELEASE_VERSION = "0.1.34";
14
+ export const CONSUMER_RELEASE_VERSION = "0.1.36";
15
15
  export const API_COMMANDS = new Set(["health", "models", "responses", "chat"]);
16
16
  const API_VALUE_OPTIONS = new Set([
17
17
  "--base-url",
@@ -111,6 +111,7 @@ export async function main(argv, dependencies = {}) {
111
111
 
112
112
  const shutdown = async () => {
113
113
  clearPid(parsed.dataDir);
114
+ await state.stopShare?.();
114
115
  await runtime.close();
115
116
  };
116
117
  if (parsed.noCodex) {