mslxdff 0.1.1 → 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
@@ -14,31 +14,42 @@ Zero runtime dependencies: Node ≥ 20, built-in `node:http`, `node:crypto`, `no
14
14
 
15
15
  ```
16
16
  npm install # no deps actually fetched; just links the bin
17
- mslxdfree # or: node bin/mslxdfree.js
17
+ mslxdff # or: node bin/mslxdff.js
18
18
  ```
19
19
 
20
20
  First run generates a bearer token, writes it to the state file, and prints it:
21
21
 
22
22
  ```
23
- mslxdfree listening on http://localhost:8080
23
+ mslxdff listening on http://localhost:8989
24
24
  auth token: 9b5de021e914...
25
- endpoint: http://localhost:8080/v1
25
+ endpoint: http://localhost:8989/v1
26
26
  ```
27
27
 
28
+ ### Port
29
+
30
+ Default port is **8989**. Persist a different port (and hot-restart the daemon onto it if one is running):
31
+
32
+ ```
33
+ mslxdff -port 8000 # set port to 8000; restarts the daemon on 8000
34
+ mslxdff -d # next starts reuse the persisted port (8000)
35
+ ```
36
+
37
+ Priority: `-port` arg > persisted port > `PORT` env > default `8989`.
38
+
28
39
  ### Daemon (background, stays resident)
29
40
 
30
41
  ```
31
- mslxdfree -d # start detached background daemon
32
- mslxdfree -stop # stop it
42
+ mslxdff -d # start detached background daemon
43
+ mslxdff -stop # stop it
33
44
  ```
34
45
 
35
- Logs go to `~/.config/mslxdfree/daemon.log`, the daemon pid to `daemon.pid` (both overridable via `MSLXDFREE_DAEMON_DIR`). The daemon keeps running after your shell exits.
46
+ Logs go to `~/.config/mslxdff/daemon.log`, the daemon pid to `daemon.pid` (both overridable via `MSLXDFF_DAEMON_DIR`). The daemon keeps running after your shell exits.
36
47
 
37
48
  ### Token
38
49
 
39
50
  ```
40
- mslxdfree -showtoken # print the current token (creates one on first use)
41
- mslxdfree -refresh-token # rotate it (prints the new token, does not start the server)
51
+ mslxdff -showtoken # print the current token (creates one on first use)
52
+ mslxdff -refresh-token # rotate it (prints the new token, does not start the server)
42
53
  ```
43
54
 
44
55
  ## Client configuration
@@ -46,7 +57,7 @@ mslxdfree -refresh-token # rotate it (prints the new token, does not start the
46
57
  Point any OpenAI-compatible client at the endpoint with the token:
47
58
 
48
59
  ```
49
- Endpoint: http://localhost:8080/v1
60
+ Endpoint: http://localhost:8989/v1
50
61
  API Key: <the bearer token> (sent as Authorization: Bearer <token>)
51
62
  Model: oc/deepseek-v4-flash-free (the oc/ prefix is optional)
52
63
  ```
@@ -54,7 +65,7 @@ Model: oc/deepseek-v4-flash-free (the oc/ prefix is optional)
54
65
  <x-model list>
55
66
 
56
67
  ```
57
- $ curl -H "Authorization: Bearer <token>" http://localhost:8080/v1/models
68
+ $ curl -H "Authorization: Bearer <token>" http://localhost:8989/v1/models
58
69
  {"object":"list","data":[{"id":"big-pickle",...},{"id":"deepseek-v4-flash-free",...}, ...]}
59
70
  </x-model list>
60
71
 
@@ -62,9 +73,9 @@ $ curl -H "Authorization: Bearer <token>" http://localhost:8080/v1/models
62
73
 
63
74
  | Variable | Default | Purpose |
64
75
  |---|---|---|
65
- | `PORT` | `8080` | listen port |
66
- | `MSLXDFREE_STATE_FILE` | `~/.config/mslxdfree/state.json` | token state file (mode 0600) |
67
- | `MSLXDFREE_DAEMON_DIR` | `~/.config/mslxdfree` | daemon pid + log directory |
76
+ | `PORT` | `8989` | listen port (used when no `-port` arg and no persisted port) |
77
+ | `MSLXDFF_STATE_FILE` | `~/.config/mslxdff/state.json` | token/port state file (mode 0600) |
78
+ | `MSLXDFF_DAEMON_DIR` | `~/.config/mslxdff` | daemon pid + log directory |
68
79
  | `UPSTREAM_BASE_URL` | `https://opencode.ai` | upstream base |
69
80
  | `UPSTREAM_AUTH_TOKEN` | `public` | upstream `Authorization: Bearer <…>` value |
70
81
  | `UPSTREAM_CONNECT_TIMEOUT_MS` | `30000` | upstream connect timeout |
@@ -72,7 +83,7 @@ $ curl -H "Authorization: Bearer <token>" http://localhost:8080/v1/models
72
83
 
73
84
  ## Clients
74
85
 
75
- Point your OpenAI client at `http://<host>:8080/v1` (or the equivalent config seen above). Works for streaming and non-streaming chat completions.
86
+ Point your OpenAI client at `http://<host>:8989/v1` (or the equivalent config seen above). Works for streaming and non-streaming chat completions.
76
87
 
77
88
  ## Development
78
89
 
@@ -80,4 +91,4 @@ Point your OpenAI client at `http://<host>:8080/v1` (or the equivalent config se
80
91
  npm test # node --test, no network access
81
92
  ```
82
93
 
83
- The reference implementation is 9Router v0.5.45 (`/root/9router`); see `CLAUDE.md`, `CONTEXT.md`, and `docs/adr/` for the contract and the decisions behind it.
94
+ The reference implementation is 9Router v0.5.45 (`/root/9router`); see `CLAUDE.md`, `CONTEXT.md`, and `docs/adr/` for the contract and the decisions behind it.
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { startServer } from "../src/server.js";
2
+ import { startServer, resolvePort } from "../src/server.js";
3
3
  import { createRouter } from "../src/routes.js";
4
4
  import { createUpstreamClient } from "../src/upstream.js";
5
5
  import { createModelsService } from "../src/models.js";
6
- import { loadToken, refreshToken } from "../src/state.js";
7
- import { startDaemon, stopDaemon, writePid, pidFile, logFile } from "../src/daemon.js";
6
+ import { loadToken, refreshToken, setPort } from "../src/state.js";
7
+ import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid } from "../src/daemon.js";
8
8
 
9
9
  const args = process.argv.slice(2);
10
10
 
@@ -23,19 +23,43 @@ if (args.includes("-showtoken") || args.includes("--showtoken")) {
23
23
  if (args.includes("-stop") || args.includes("--stop")) {
24
24
  const { stopped, pid, reason } = stopDaemon();
25
25
  if (stopped) {
26
- console.log(`mslxdfree daemon stopped (pid ${pid})`);
26
+ console.log(`mslxdff daemon stopped (pid ${pid})`);
27
27
  } else {
28
- console.log(`mslxdfree daemon not running${reason ? ` (${reason})` : ""}`);
28
+ console.log(`mslxdff daemon not running${reason ? ` (${reason})` : ""}`);
29
+ }
30
+ process.exit(0);
31
+ }
32
+
33
+ // -port N: persist the port, then restart the daemon on it if one is running.
34
+ // Skip when we ARE the daemon child (it already carries the port via args).
35
+ const portArg = argValue("-port", "--port");
36
+ if (portArg && !process.env.MSLXDFF_DAEMON) {
37
+ const port = Number(portArg);
38
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
39
+ console.error(`invalid port: ${portArg}`);
40
+ process.exit(1);
41
+ }
42
+ setPort(port);
43
+ const daemon = readPid();
44
+ if (daemon) {
45
+ stopDaemon();
46
+ startDaemon(["-port", String(port)]);
47
+ await waitForHealth(port, 4000);
48
+ console.log(`mslxdff restarted on port ${port} (pid ${readPid()})`);
49
+ console.log(`endpoint: http://localhost:${port}/v1`);
50
+ } else {
51
+ console.log(`port saved: ${port} (daemon not running; takes effect on next start)`);
29
52
  }
30
53
  process.exit(0);
31
54
  }
32
55
 
33
56
  if (args.includes("-d") || args.includes("--daemon")) {
34
- if (!process.env.MSLXDFREE_DAEMON) {
57
+ if (!process.env.MSLXDFF_DAEMON) {
35
58
  // foreground: spawn the detached background instance, then wait for health
59
+ const port = effectivePort();
36
60
  const spawnedPid = startDaemon(args.filter((a) => a !== "-d" && a !== "--daemon"));
37
- await waitForHealth(4000);
38
- console.log(`mslxdfree daemon started (pid ${spawnedPid})`);
61
+ await waitForHealth(port, 4000);
62
+ console.log(`mslxdff daemon started (pid ${spawnedPid})`);
39
63
  console.log(`log: ${logFile()}`);
40
64
  console.log(`pid: ${pidFile()}`);
41
65
  process.exit(0);
@@ -52,20 +76,32 @@ const router = createRouter({ token, upstream, models });
52
76
  const srv = startServer({ router });
53
77
 
54
78
  await srv.ready();
55
- if (process.env.MSLXDFREE_DAEMON) {
79
+ if (process.env.MSLXDFF_DAEMON) {
56
80
  writePid(process.pid);
57
81
  }
58
82
  const addr = srv.server.address();
59
83
  const host = addr.address === "0.0.0.0" || addr.address === "::" ? "localhost" : addr.address;
60
- console.log(`mslxdfree listening on http://${host}:${addr.port}`);
84
+ console.log(`mslxdff listening on http://${host}:${addr.port}`);
61
85
  if (created) {
62
86
  console.log(`auth token: ${token}`);
63
87
  }
64
88
  console.log(`endpoint: http://${host}:${addr.port}/v1`);
65
89
 
66
- async function waitForHealth(timeoutMs) {
90
+ function argValue(...names) {
91
+ for (let i = 0; i < args.length; i++) {
92
+ if (names.includes(args[i])) return args[i + 1];
93
+ }
94
+ return null;
95
+ }
96
+
97
+ function effectivePort() {
98
+ const arg = argValue("-port", "--port");
99
+ if (arg) return Number(arg);
100
+ return resolvePort();
101
+ }
102
+
103
+ async function waitForHealth(port, timeoutMs) {
67
104
  const start = Date.now();
68
- const port = Number(process.env.PORT) || 8080;
69
105
  while (Date.now() - start < timeoutMs) {
70
106
  try {
71
107
  const res = await fetch(`http://127.0.0.1:${port}/health`);
@@ -75,4 +111,4 @@ async function waitForHealth(timeoutMs) {
75
111
  }
76
112
  await new Promise((r) => setTimeout(r, 100));
77
113
  }
78
- }
114
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
7
- "mslxdfree": "bin/mslxdfree.js"
7
+ "mslxdff": "bin/mslxdff.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node bin/mslxdfree.js",
10
+ "start": "node bin/mslxdff.js",
11
11
  "test": "node --test test/"
12
12
  },
13
13
  "engines": {
package/src/daemon.js CHANGED
@@ -5,7 +5,7 @@ import os from "node:os";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  export function daemonDir() {
8
- return process.env.MSLXDFREE_DAEMON_DIR || join(os.homedir(), ".config", "mslxdfree");
8
+ return process.env.MSLXDFF_DAEMON_DIR || join(os.homedir(), ".config", "mslxdff");
9
9
  }
10
10
 
11
11
  export function pidFile() {
@@ -18,16 +18,16 @@ export function logFile() {
18
18
 
19
19
  export function startDaemon(args = []) {
20
20
  const here = fileURLToPath(import.meta.url);
21
- const entry = here.endsWith("bin/mslxdfree.js")
21
+ const entry = here.endsWith("bin/mslxdff.js")
22
22
  ? here
23
- : join(dirname(here), "..", "bin", "mslxdfree.js");
23
+ : join(dirname(here), "..", "bin", "mslxdff.js");
24
24
  const dir = daemonDir();
25
25
  mkdirSync(dir, { recursive: true });
26
26
  const logFd = openSync(logFile(), "a", 0o600);
27
27
  const child = spawn(process.execPath, [entry, ...args, "--daemon"], {
28
28
  detached: true,
29
29
  stdio: ["ignore", logFd, logFd],
30
- env: { ...process.env, MSLXDFREE_DAEMON: "1" },
30
+ env: { ...process.env, MSLXDFF_DAEMON: "1" },
31
31
  });
32
32
  child.unref();
33
33
  return child.pid;
package/src/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createServer as httpCreateServer } from "node:http";
2
+ import { DEFAULT_PORT, getPort } from "./state.js";
2
3
 
3
- export function startServer({ router }, port = Number(process.env.PORT) || 8080) {
4
+ export function startServer({ router }, port = resolvePort()) {
4
5
  const server = httpCreateServer((req, res) => {
5
6
  router(req, res).catch((err) => {
6
7
  res.statusCode = 500;
@@ -24,4 +25,12 @@ export function startServer({ router }, port = Number(process.env.PORT) || 8080)
24
25
  process.on("SIGTERM", close);
25
26
 
26
27
  return { server, ready, close };
27
- }
28
+ }
29
+
30
+ export function resolvePort() {
31
+ const persisted = getPort();
32
+ if (persisted) return persisted;
33
+ const env = Number(process.env.PORT);
34
+ if (Number.isInteger(env) && env > 0) return env;
35
+ return DEFAULT_PORT;
36
+ }
package/src/state.js CHANGED
@@ -3,9 +3,11 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import os from "node:os";
5
5
 
6
+ export const DEFAULT_PORT = 8989;
7
+
6
8
  export function defaultStateFile() {
7
- return process.env.MSLXDFREE_STATE_FILE ||
8
- join(os.homedir(), ".config", "mslxdfree", "state.json");
9
+ return process.env.MSLXDFF_STATE_FILE ||
10
+ join(os.homedir(), ".config", "mslxdff", "state.json");
9
11
  }
10
12
 
11
13
  export function generateToken() {
@@ -13,27 +15,39 @@ export function generateToken() {
13
15
  }
14
16
 
15
17
  export async function loadToken({ file = defaultStateFile() } = {}) {
16
- try {
17
- const saved = JSON.parse(readFileSync(file, "utf8"));
18
- if (typeof saved.token === "string" && saved.token.length > 0) {
19
- return { token: saved.token, created: false };
20
- }
21
- } catch {
22
- // missing or unreadable → generate fresh below
18
+ const state = readState(file);
19
+ if (typeof state.token === "string" && state.token.length > 0) {
20
+ return { token: state.token, created: false };
23
21
  }
24
- return { token: writeToken(file), created: true };
22
+ return { token: writeState(file, { token: generateToken(), createdAt: new Date().toISOString() }).token, created: true };
25
23
  }
26
24
 
27
25
  export async function refreshToken({ file = defaultStateFile() } = {}) {
28
- return writeToken(file);
26
+ return writeState(file, { token: generateToken(), createdAt: new Date().toISOString() }).token;
27
+ }
28
+
29
+ export function setPort(port, { file = defaultStateFile() } = {}) {
30
+ const state = readState(file);
31
+ writeState(file, { ...state, port: Number(port) });
32
+ }
33
+
34
+ export function getPort({ file = defaultStateFile() } = {}) {
35
+ const port = readState(file).port;
36
+ return typeof port === "number" && Number.isInteger(port) && port > 0 ? port : null;
29
37
  }
30
38
 
31
- function writeToken(file) {
32
- const state = {
33
- token: generateToken(),
34
- createdAt: new Date().toISOString(),
35
- };
39
+ function readState(file) {
40
+ try {
41
+ const saved = JSON.parse(readFileSync(file, "utf8"));
42
+ return typeof saved === "object" && saved !== null ? saved : {};
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
47
+
48
+ function writeState(file, patch) {
49
+ const merged = { ...readState(file), ...patch };
36
50
  mkdirSync(dirname(file), { recursive: true });
37
- writeFileSync(file, JSON.stringify(state, null, 2), { mode: 0o600 });
38
- return state.token;
39
- }
51
+ writeFileSync(file, JSON.stringify(merged, null, 2), { mode: 0o600 });
52
+ return merged;
53
+ }