@mulmobridge/client 1.0.1 → 1.1.0
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 +35 -1
- package/dist/apiUrl.d.ts +32 -0
- package/dist/apiUrl.js +70 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.js +17 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/mime.js +4 -1
- package/dist/token.d.ts +13 -0
- package/dist/token.js +22 -14
- package/dist/workspace.d.ts +23 -0
- package/dist/workspace.js +54 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ yarn add @mulmobridge/client
|
|
|
17
17
|
| `createBridgeClient(opts)` | Create a connected socket.io client with auth |
|
|
18
18
|
| `requireBearerToken()` | Read the bearer token or exit with a helpful message |
|
|
19
19
|
| `readBridgeToken()` | Read the bearer token (returns `null` if absent) |
|
|
20
|
-
| `TOKEN_FILE_PATH` | Path to
|
|
20
|
+
| `TOKEN_FILE_PATH` | Path to the workspace's `.session-token` |
|
|
21
|
+
| `resolveApiUrl(explicit?)` | Resolve the server URL a bridge should connect to |
|
|
21
22
|
| `mimeFromExtension(ext)` | Map file extension to MIME type |
|
|
22
23
|
| `isImageMime(mime)` | Check if MIME is an image type |
|
|
23
24
|
| `isPdfMime(mime)` | Check if MIME is PDF |
|
|
@@ -47,6 +48,39 @@ client.onPush((ev) => {
|
|
|
47
48
|
});
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
## Which server it connects to
|
|
52
|
+
|
|
53
|
+
The MulmoClaude server is **not pinned to port 3001**. It honours `PORT`, and an
|
|
54
|
+
implicit default that is already busy walks forward (`Port 3001 busy → using 3002
|
|
55
|
+
instead`). Whatever it ends up binding, it publishes to `<workspace>/.server-port`
|
|
56
|
+
— the file every out-of-process reader uses to find it.
|
|
57
|
+
|
|
58
|
+
`createBridgeClient()` resolves the address in this order:
|
|
59
|
+
|
|
60
|
+
1. `opts.apiUrl` — an explicit value always wins
|
|
61
|
+
2. `$MULMOCLAUDE_API_URL`
|
|
62
|
+
3. `http://127.0.0.1:<port>` from `<workspace>/.server-port`
|
|
63
|
+
4. `http://localhost:3001`
|
|
64
|
+
|
|
65
|
+
The workspace itself is `$MULMOCLAUDE_WORKSPACE_PATH`, or `~/mulmoclaude` when
|
|
66
|
+
that is unset — the same rule the server applies, and the same root the bearer
|
|
67
|
+
token is read from.
|
|
68
|
+
|
|
69
|
+
Only `process.env` is consulted. A `.env` file reaches this library through the
|
|
70
|
+
bridge's own `import "dotenv/config"`, which resolves `.env` against the
|
|
71
|
+
process's **current working directory** — so a bridge launched from somewhere
|
|
72
|
+
else does not see a `MULMOCLAUDE_WORKSPACE_PATH` that lives only in the repo's
|
|
73
|
+
`.env`, exactly as it would not see `MULMOCLAUDE_AUTH_TOKEN` there. Export the
|
|
74
|
+
variable, or run the bridge from the directory holding the `.env`.
|
|
75
|
+
|
|
76
|
+
| Export | Resolves |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `readBridgeToken()` / `tokenFilePath()` | at call time |
|
|
79
|
+
| `TOKEN_FILE_PATH` | at import time — a snapshot, kept for compatibility |
|
|
80
|
+
|
|
81
|
+
The port is read once, when the client is created. A server that restarts onto a
|
|
82
|
+
*different* port after that still needs the bridge restarted.
|
|
83
|
+
|
|
50
84
|
## Ecosystem
|
|
51
85
|
|
|
52
86
|
Part of the [`@mulmobridge/*`](https://www.npmjs.com/~mulmobridge) package family.
|
package/dist/apiUrl.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Used only when nothing has been published and nothing was configured. */
|
|
2
|
+
export declare const DEFAULT_API_URL = "http://localhost:3001";
|
|
3
|
+
/**
|
|
4
|
+
* The published port, or `null` for anything that is not one.
|
|
5
|
+
*
|
|
6
|
+
* Decimal digits only. The writer is `formatServerPort()`, which emits
|
|
7
|
+
* `${port}\n` and nothing else, so there is no lenient shape worth
|
|
8
|
+
* accepting — while `Number.parseInt` would read `3002abc` as 3002 and send
|
|
9
|
+
* the bridge to a port a corrupted file never meant.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parsePublishedPort(raw: string | null): number | null;
|
|
12
|
+
/**
|
|
13
|
+
* The origin the running server published, or `null` if it published none.
|
|
14
|
+
*
|
|
15
|
+
* `127.0.0.1` rather than `localhost`: the server binds the IPv4 loopback
|
|
16
|
+
* explicitly (`app.listen(port, "127.0.0.1")`), while `localhost` resolves
|
|
17
|
+
* to `::1` first on a dual-stack host. Usually that still works — nothing
|
|
18
|
+
* holds `::1`, the connection is refused and Node falls back to IPv4 — but
|
|
19
|
+
* when something IS there the client reaches it and never falls back, which
|
|
20
|
+
* is the silent misdirection this module exists to remove (#2981).
|
|
21
|
+
*/
|
|
22
|
+
export declare function readPublishedApiUrl(): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Resolution order: explicit argument → `MULMOCLAUDE_API_URL` → the port the
|
|
25
|
+
* server published → `DEFAULT_API_URL`.
|
|
26
|
+
*
|
|
27
|
+
* An explicit value still wins, so nothing that already sets one changes.
|
|
28
|
+
* An EMPTY value falls through instead of being used verbatim, matching how
|
|
29
|
+
* `readBridgeToken` treats an empty `MULMOCLAUDE_AUTH_TOKEN` — an empty
|
|
30
|
+
* string reached `io("")` before this.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveApiUrl(explicit?: string): string;
|
package/dist/apiUrl.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Which server a bridge talks to (#3078).
|
|
2
|
+
//
|
|
3
|
+
// The server is not pinned to 3001. `server/index.ts` walks forward off a
|
|
4
|
+
// busy default (`Port 3001 busy → using 3002 instead`) and honours `PORT`,
|
|
5
|
+
// then publishes whatever it actually bound to `<workspace>/.server-port`.
|
|
6
|
+
// A client that does not read that file either connects to nothing (case A:
|
|
7
|
+
// `PORT=3099` and nobody on 3001) or — worse — connects cleanly to a
|
|
8
|
+
// DIFFERENT instance that happens to hold 3001 (case B), which with a shared
|
|
9
|
+
// `MULMOCLAUDE_AUTH_TOKEN` authenticates without a single error.
|
|
10
|
+
//
|
|
11
|
+
// This is the same class as #2650 (Vite's proxy target) and #2981
|
|
12
|
+
// (`wait-for-backend`); both were fixed by reading the published port.
|
|
13
|
+
//
|
|
14
|
+
// A leftover `.server-port` cannot mislead here the way it can mislead
|
|
15
|
+
// `yarn dev`: the file is not removed on shutdown, but the server REWRITES it
|
|
16
|
+
// on every startup, so a running server's entry is always current — and with
|
|
17
|
+
// no server running, the old hardcoded 3001 was just as dead.
|
|
18
|
+
import { readSidecarFile, SIDECAR_FILES } from "./workspace.js";
|
|
19
|
+
/** Used only when nothing has been published and nothing was configured. */
|
|
20
|
+
export const DEFAULT_API_URL = "http://localhost:3001";
|
|
21
|
+
const MIN_PORT = 1;
|
|
22
|
+
const MAX_PORT = 65_535;
|
|
23
|
+
/**
|
|
24
|
+
* The published port, or `null` for anything that is not one.
|
|
25
|
+
*
|
|
26
|
+
* Decimal digits only. The writer is `formatServerPort()`, which emits
|
|
27
|
+
* `${port}\n` and nothing else, so there is no lenient shape worth
|
|
28
|
+
* accepting — while `Number.parseInt` would read `3002abc` as 3002 and send
|
|
29
|
+
* the bridge to a port a corrupted file never meant.
|
|
30
|
+
*/
|
|
31
|
+
export function parsePublishedPort(raw) {
|
|
32
|
+
if (raw === null)
|
|
33
|
+
return null;
|
|
34
|
+
const trimmed = raw.trim();
|
|
35
|
+
if (!/^\d+$/.test(trimmed))
|
|
36
|
+
return null;
|
|
37
|
+
const port = Number.parseInt(trimmed, 10);
|
|
38
|
+
return port >= MIN_PORT && port <= MAX_PORT ? port : null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The origin the running server published, or `null` if it published none.
|
|
42
|
+
*
|
|
43
|
+
* `127.0.0.1` rather than `localhost`: the server binds the IPv4 loopback
|
|
44
|
+
* explicitly (`app.listen(port, "127.0.0.1")`), while `localhost` resolves
|
|
45
|
+
* to `::1` first on a dual-stack host. Usually that still works — nothing
|
|
46
|
+
* holds `::1`, the connection is refused and Node falls back to IPv4 — but
|
|
47
|
+
* when something IS there the client reaches it and never falls back, which
|
|
48
|
+
* is the silent misdirection this module exists to remove (#2981).
|
|
49
|
+
*/
|
|
50
|
+
export function readPublishedApiUrl() {
|
|
51
|
+
const port = parsePublishedPort(readSidecarFile(SIDECAR_FILES.port));
|
|
52
|
+
return port === null ? null : `http://127.0.0.1:${port}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolution order: explicit argument → `MULMOCLAUDE_API_URL` → the port the
|
|
56
|
+
* server published → `DEFAULT_API_URL`.
|
|
57
|
+
*
|
|
58
|
+
* An explicit value still wins, so nothing that already sets one changes.
|
|
59
|
+
* An EMPTY value falls through instead of being used verbatim, matching how
|
|
60
|
+
* `readBridgeToken` treats an empty `MULMOCLAUDE_AUTH_TOKEN` — an empty
|
|
61
|
+
* string reached `io("")` before this.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveApiUrl(explicit) {
|
|
64
|
+
if (typeof explicit === "string" && explicit.length > 0)
|
|
65
|
+
return explicit;
|
|
66
|
+
const fromEnv = process.env.MULMOCLAUDE_API_URL;
|
|
67
|
+
if (typeof fromEnv === "string" && fromEnv.length > 0)
|
|
68
|
+
return fromEnv;
|
|
69
|
+
return readPublishedApiUrl() ?? DEFAULT_API_URL;
|
|
70
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -14,7 +14,9 @@ export interface BridgeClientOptions {
|
|
|
14
14
|
/** Required. Identifier for this bridge in the handshake.
|
|
15
15
|
* Matches `handshake.auth.transportId` server-side. */
|
|
16
16
|
transportId: string;
|
|
17
|
-
/** Defaults to `$MULMOCLAUDE_API_URL
|
|
17
|
+
/** Defaults to `$MULMOCLAUDE_API_URL`, then the port the server
|
|
18
|
+
* published to `<workspace>/.server-port`, then
|
|
19
|
+
* `http://localhost:3001` (#3078). */
|
|
18
20
|
apiUrl?: string;
|
|
19
21
|
/** Flat primitive bag forwarded to the host app's startChat
|
|
20
22
|
* callback via the handshake (`BridgeOptions` from the
|
package/dist/client.js
CHANGED
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
// minimal non-Node equivalent.
|
|
14
14
|
import { io } from "socket.io-client";
|
|
15
15
|
import { CHAT_SOCKET_EVENTS, CHAT_SOCKET_PATH } from "@mulmobridge/protocol";
|
|
16
|
-
import { readBridgeToken,
|
|
16
|
+
import { readBridgeToken, tokenFilePath } from "./token.js";
|
|
17
17
|
import { readBridgeEnvOptions } from "./options.js";
|
|
18
|
+
import { resolveApiUrl } from "./apiUrl.js";
|
|
18
19
|
// 6 min > the server's REPLY_TIMEOUT_MS (5 min) so the server's
|
|
19
20
|
// timeout surfaces as a reply, not a client-side cancellation.
|
|
20
21
|
const REPLY_TIMEOUT_MS = 6 * 60 * 1000;
|
|
21
|
-
const DEFAULT_API_URL = "http://localhost:3001";
|
|
22
22
|
/**
|
|
23
23
|
* Resolve the bearer token from the workspace / env var, exit with
|
|
24
24
|
* a clear error if absent. Kept separate so bridges that want to
|
|
@@ -29,16 +29,29 @@ export function requireBearerToken() {
|
|
|
29
29
|
const token = readBridgeToken();
|
|
30
30
|
if (token !== null)
|
|
31
31
|
return token;
|
|
32
|
+
// `tokenFilePath()` rather than `TOKEN_FILE_PATH`, which is fixed at module
|
|
33
|
+
// load: a bridge that imports this before `dotenv/config` would otherwise be
|
|
34
|
+
// told to look somewhere the token was never going to be.
|
|
32
35
|
process.stderr.write(`No bearer token found. The MulmoClaude server writes one to\n` +
|
|
33
|
-
` ${
|
|
36
|
+
` ${tokenFilePath()}\n` +
|
|
34
37
|
`at startup (mode 0600). Start the server with \`yarn dev\` (or\n` +
|
|
35
38
|
`\`npm run dev\`) first, or set MULMOCLAUDE_AUTH_TOKEN to the\n` +
|
|
36
39
|
`same value the server is using.\n`);
|
|
37
40
|
return process.exit(1);
|
|
38
41
|
}
|
|
39
42
|
export function createBridgeClient(opts) {
|
|
40
|
-
|
|
43
|
+
// Token BEFORE port. A restart rewrites both files and nothing marks them as
|
|
44
|
+
// one generation, so a bridge starting mid-restart can read a torn pair in
|
|
45
|
+
// either order. What the order decides is WHICH tear it gets. Port first
|
|
46
|
+
// yields a NEW token with an OLD port — a fresh credential sent to the port
|
|
47
|
+
// the server has just left, retried in silence because the socket's URL is
|
|
48
|
+
// fixed at construction. Token first mostly yields the opposite, an OLD token
|
|
49
|
+
// with a NEW port, which the right server answers `invalid token` and the
|
|
50
|
+
// connect handler explains; the dangerous pairing survives only in the narrow
|
|
51
|
+
// window where BOTH reads fall between the token write and the port publish.
|
|
52
|
+
// Closing it needs a shared generation marker on the sidecars (Codex, #3082).
|
|
41
53
|
const token = requireBearerToken();
|
|
54
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
42
55
|
// `opts.options === undefined` → scrape env automatically.
|
|
43
56
|
// `opts.options === {}` → opt out of the scrape explicitly.
|
|
44
57
|
const options = opts.options ?? readBridgeEnvOptions(opts.transportId, process.env);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createBridgeClient, requireBearerToken, type MessageAck, type PushEvent, type BridgeClientOptions, type BridgeClient } from "./client.js";
|
|
2
|
-
export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
|
|
2
|
+
export { readBridgeToken, tokenFilePath, TOKEN_FILE_PATH } from "./token.js";
|
|
3
|
+
export { resolveApiUrl } from "./apiUrl.js";
|
|
3
4
|
export { readBridgeEnvOptions } from "./options.js";
|
|
4
5
|
export { chunkText } from "./text.js";
|
|
5
6
|
export { frameText } from "./frame.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @mulmobridge/client — shared socket.io client for all MulmoBridge bridges.
|
|
2
2
|
export { createBridgeClient, requireBearerToken } from "./client.js";
|
|
3
|
-
export { readBridgeToken, TOKEN_FILE_PATH } from "./token.js";
|
|
3
|
+
export { readBridgeToken, tokenFilePath, TOKEN_FILE_PATH } from "./token.js";
|
|
4
|
+
export { resolveApiUrl } from "./apiUrl.js";
|
|
4
5
|
export { readBridgeEnvOptions } from "./options.js";
|
|
5
6
|
export { chunkText } from "./text.js";
|
|
6
7
|
export { frameText } from "./frame.js";
|
package/dist/mime.js
CHANGED
|
@@ -57,7 +57,10 @@ export function parseDataUrl(dataUrl) {
|
|
|
57
57
|
const match = dataUrl.match(/^data:([^;,]+)(?:;[^,]*)?;base64,(.+)$/);
|
|
58
58
|
if (!match)
|
|
59
59
|
return null;
|
|
60
|
-
|
|
60
|
+
const [, mimeType, data] = match;
|
|
61
|
+
if (mimeType === undefined || data === undefined)
|
|
62
|
+
return null;
|
|
63
|
+
return { mimeType, data };
|
|
61
64
|
}
|
|
62
65
|
/** Build a `data:<mime>;base64,<data>` string from components. */
|
|
63
66
|
export function buildDataUrl(mimeType, base64Data) {
|
package/dist/token.d.ts
CHANGED
|
@@ -1,2 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the token file is, resolved NOW.
|
|
3
|
+
*
|
|
4
|
+
* Prefer this over `TOKEN_FILE_PATH` for anything that reads, writes, or
|
|
5
|
+
* reports the path: the constant freezes the workspace root at import time, so
|
|
6
|
+
* it disagrees with `readBridgeToken()` for any process that sets
|
|
7
|
+
* `MULMOCLAUDE_WORKSPACE_PATH` after importing this package (Codex, #3078).
|
|
8
|
+
*/
|
|
9
|
+
export declare function tokenFilePath(): string;
|
|
10
|
+
/** Public since #272, so it stays. It now honours `MULMOCLAUDE_WORKSPACE_PATH`
|
|
11
|
+
* (#3078) instead of hardcoding `<homedir>/mulmoclaude`, but it is a snapshot
|
|
12
|
+
* taken at import time — use `tokenFilePath()` unless you specifically want
|
|
13
|
+
* the value as it stood when the module loaded. */
|
|
1
14
|
export declare const TOKEN_FILE_PATH: string;
|
|
2
15
|
export declare function readBridgeToken(): string | null;
|
package/dist/token.js
CHANGED
|
@@ -4,26 +4,34 @@
|
|
|
4
4
|
// Resolution order:
|
|
5
5
|
// 1. `MULMOCLAUDE_AUTH_TOKEN` env var (useful for parallel shells,
|
|
6
6
|
// CI, or when the user runs the bridge against a different
|
|
7
|
-
// workspace than
|
|
8
|
-
// 2. `<
|
|
9
|
-
//
|
|
7
|
+
// workspace than the server's)
|
|
8
|
+
// 2. `<workspace>/.session-token` — the file the server writes at
|
|
9
|
+
// startup. Same path the Vite dev plugin reads from, and the
|
|
10
|
+
// pair of `.server-port` (see `workspace.ts`).
|
|
10
11
|
//
|
|
11
12
|
// Returns null if neither source yields a non-empty string — the
|
|
12
13
|
// caller decides how to react (exit with a helpful message, in the
|
|
13
14
|
// bridge's case).
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
import { readSidecarFile, SIDECAR_FILES, sidecarPath } from "./workspace.js";
|
|
16
|
+
/**
|
|
17
|
+
* Where the token file is, resolved NOW.
|
|
18
|
+
*
|
|
19
|
+
* Prefer this over `TOKEN_FILE_PATH` for anything that reads, writes, or
|
|
20
|
+
* reports the path: the constant freezes the workspace root at import time, so
|
|
21
|
+
* it disagrees with `readBridgeToken()` for any process that sets
|
|
22
|
+
* `MULMOCLAUDE_WORKSPACE_PATH` after importing this package (Codex, #3078).
|
|
23
|
+
*/
|
|
24
|
+
export function tokenFilePath() {
|
|
25
|
+
return sidecarPath(SIDECAR_FILES.token);
|
|
26
|
+
}
|
|
27
|
+
/** Public since #272, so it stays. It now honours `MULMOCLAUDE_WORKSPACE_PATH`
|
|
28
|
+
* (#3078) instead of hardcoding `<homedir>/mulmoclaude`, but it is a snapshot
|
|
29
|
+
* taken at import time — use `tokenFilePath()` unless you specifically want
|
|
30
|
+
* the value as it stood when the module loaded. */
|
|
31
|
+
export const TOKEN_FILE_PATH = sidecarPath(SIDECAR_FILES.token);
|
|
18
32
|
export function readBridgeToken() {
|
|
19
33
|
const fromEnv = process.env.MULMOCLAUDE_AUTH_TOKEN;
|
|
20
34
|
if (typeof fromEnv === "string" && fromEnv.length > 0)
|
|
21
35
|
return fromEnv;
|
|
22
|
-
|
|
23
|
-
const raw = fs.readFileSync(TOKEN_FILE_PATH, "utf-8").trim();
|
|
24
|
-
return raw.length > 0 ? raw : null;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
36
|
+
return readSidecarFile(SIDECAR_FILES.token);
|
|
29
37
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Sidecar files the server rewrites on every startup. */
|
|
2
|
+
export declare const SIDECAR_FILES: {
|
|
3
|
+
readonly token: ".session-token";
|
|
4
|
+
readonly port: ".server-port";
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The workspace root the server is using.
|
|
8
|
+
*
|
|
9
|
+
* Same rule as the server's own `workspacePath`
|
|
10
|
+
* (`server/workspace/paths.ts`): `MULMOCLAUDE_WORKSPACE_PATH` wins,
|
|
11
|
+
* otherwise `<homedir>/mulmoclaude`. The server's extra test-env
|
|
12
|
+
* branch is deliberately not mirrored — it isolates the server's own
|
|
13
|
+
* integration runs and means nothing to a bridge process.
|
|
14
|
+
*/
|
|
15
|
+
export declare function workspaceRoot(): string;
|
|
16
|
+
/** Absolute path of one sidecar file inside the workspace. */
|
|
17
|
+
export declare function sidecarPath(fileName: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Sidecar contents, trimmed. `null` when the file is absent,
|
|
20
|
+
* unreadable, or holds nothing but whitespace — all of which mean the
|
|
21
|
+
* same thing to a caller: the server has not told us this yet.
|
|
22
|
+
*/
|
|
23
|
+
export declare function readSidecarFile(fileName: string): string | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Where a bridge looks for the sidecar files the server writes on every
|
|
2
|
+
// startup (#3078).
|
|
3
|
+
//
|
|
4
|
+
// The server publishes two files into its workspace root at boot:
|
|
5
|
+
// `.session-token` (the bearer token the bridge presents) and
|
|
6
|
+
// `.server-port` (the port it actually bound, which is NOT always the
|
|
7
|
+
// one it was asked for — see `server/workspace/serverPort.ts`). They
|
|
8
|
+
// are a PAIR, rewritten together on every restart, so both are
|
|
9
|
+
// resolved from one root here rather than each growing its own idea of
|
|
10
|
+
// where the workspace is. Reading only one of them is how the bridges
|
|
11
|
+
// ended up following the token across restarts while still addressing
|
|
12
|
+
// a hardcoded `localhost:3001`.
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
const DEFAULT_WORKSPACE_DIR = "mulmoclaude";
|
|
17
|
+
/** Sidecar files the server rewrites on every startup. */
|
|
18
|
+
export const SIDECAR_FILES = {
|
|
19
|
+
token: ".session-token",
|
|
20
|
+
port: ".server-port",
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* The workspace root the server is using.
|
|
24
|
+
*
|
|
25
|
+
* Same rule as the server's own `workspacePath`
|
|
26
|
+
* (`server/workspace/paths.ts`): `MULMOCLAUDE_WORKSPACE_PATH` wins,
|
|
27
|
+
* otherwise `<homedir>/mulmoclaude`. The server's extra test-env
|
|
28
|
+
* branch is deliberately not mirrored — it isolates the server's own
|
|
29
|
+
* integration runs and means nothing to a bridge process.
|
|
30
|
+
*/
|
|
31
|
+
export function workspaceRoot() {
|
|
32
|
+
const configured = process.env.MULMOCLAUDE_WORKSPACE_PATH;
|
|
33
|
+
if (typeof configured === "string" && configured.length > 0)
|
|
34
|
+
return configured;
|
|
35
|
+
return path.join(os.homedir(), DEFAULT_WORKSPACE_DIR);
|
|
36
|
+
}
|
|
37
|
+
/** Absolute path of one sidecar file inside the workspace. */
|
|
38
|
+
export function sidecarPath(fileName) {
|
|
39
|
+
return path.join(workspaceRoot(), fileName);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Sidecar contents, trimmed. `null` when the file is absent,
|
|
43
|
+
* unreadable, or holds nothing but whitespace — all of which mean the
|
|
44
|
+
* same thing to a caller: the server has not told us this yet.
|
|
45
|
+
*/
|
|
46
|
+
export function readSidecarFile(fileName) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(sidecarPath(fileName), "utf-8").trim();
|
|
49
|
+
return raw.length > 0 ? raw : null;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mulmobridge/client",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Socket.io client library for MulmoBridge — shared by all bridge implementations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"author": "Receptron Team",
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@mulmobridge/protocol": "^1.0.1",
|
|
49
|
-
"@mulmoclaude/common": "^1.
|
|
49
|
+
"@mulmoclaude/common": "^1.2.0",
|
|
50
50
|
"socket.io-client": "^4.0.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@types/node": "^26.
|
|
53
|
+
"@types/node": "^26.4.1",
|
|
54
54
|
"typescript": "^6.0.3"
|
|
55
55
|
},
|
|
56
56
|
"homepage": "https://github.com/receptron/mulmoclaude/tree/main/packages/client#readme",
|