poseref 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 +9 -2
- package/dist-server/cli.js +175 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ npx poseref setup
|
|
|
20
20
|
終わったら Claude Code / Codex を起動し直し、「PoseRef で、椅子に座って頬杖をつくポーズを斜め前から撮って」のように頼んでください。
|
|
21
21
|
エージェントが最初に PoseRef のツールを使ったときに、PoseRef が起動してブラウザで画面が開きます。
|
|
22
22
|
3D の描画はブラウザで行うので、このタブは開いたままにしてください。ヘッダーに「AI連携: 接続中」と表示されていれば準備できています。
|
|
23
|
+
タブを閉じてしまっても、次にツールを使ったときに開き直します(ユーザーがわざと閉じた直後に何度も開かないよう、自動で開くのは 60 秒に 1 回までです)。
|
|
24
|
+
「PoseRef を開いて」と頼むと、エージェントが `open_poseref` で画面を開きます。すでにタブが開いていれば、新しいタブは増やしません。
|
|
23
25
|
|
|
24
26
|
登録するコマンドはバージョンを固定しています(`npx -y poseref@<バージョン> mcp`)。新しい版が公開されても、知らないうちに別のコードが動くことはありません。
|
|
25
27
|
更新するときは `npx poseref@latest setup` を実行し直してください。
|
|
@@ -42,8 +44,9 @@ npx poseref stop # 起動中の PoseRef を止める
|
|
|
42
44
|
|
|
43
45
|
### Claude Code と Codex の確認
|
|
44
46
|
|
|
45
|
-
Codex は、読み取り専用ではない MCP ツールを呼ぶたびに承認を求めます。`set_scene` と `reach` は PoseRef
|
|
46
|
-
|
|
47
|
+
Codex は、読み取り専用ではない MCP ツールを呼ぶたびに承認を求めます。`set_scene` と `reach` は PoseRef の画面だけを変え、
|
|
48
|
+
「元に戻す」で取り消せる操作で、`open_poseref` は PoseRef の画面を開くだけの操作です。そのため `npx poseref setup` は、この 3 つを承認なしで実行する設定を
|
|
49
|
+
`~/.codex/config.toml` に追記します。
|
|
47
50
|
追記の前に同じ場所へバックアップを作り、Codex が設定を読めなくなった場合は元に戻します。
|
|
48
51
|
この設定がないと、ポーズを直すたびに承認することになり、`codex exec` のような非対話の実行では `set_scene` がキャンセルされます。
|
|
49
52
|
|
|
@@ -54,6 +57,7 @@ Claude Code もツールを呼ぶたびに許可を求めることがありま
|
|
|
54
57
|
|
|
55
58
|
| ツール | 内容 |
|
|
56
59
|
| --- | --- |
|
|
60
|
+
| `open_poseref` | PoseRef が止まっていれば起動し、ブラウザで画面を開く。タブがすでに開いていれば何もしない |
|
|
57
61
|
| `get_pose_spec` | ボーンの軸と符号の決まり、接地、カメラ、プリセットの実例をまとめた説明書を返す。内容はコードの定数から生成される |
|
|
58
62
|
| `get_scene` | 現在のポーズ・キャラクターの向きと位置・カメラ・アスペクト比を JSON で返す |
|
|
59
63
|
| `set_scene` | ポーズ・キャラクター・カメラ・アスペクト比を適用する。ポーズを指定したときは、体の最下点が床にちょうど着くよう腰の高さを自動で合わせる(ジャンプなどは `pose.ground: false` で無効にできる)。未知のボーン名や範囲外の値は、何も適用せずにエラーを返す |
|
|
@@ -129,6 +133,9 @@ approval_mode = "approve"
|
|
|
129
133
|
|
|
130
134
|
[mcp_servers.poseref-dev.tools.reach]
|
|
131
135
|
approval_mode = "approve"
|
|
136
|
+
|
|
137
|
+
[mcp_servers.poseref-dev.tools.open_poseref]
|
|
138
|
+
approval_mode = "approve"
|
|
132
139
|
```
|
|
133
140
|
|
|
134
141
|
npm への公開は `npm publish` で行います。`prepublishOnly` でビルドとテストが走ります。
|
package/dist-server/cli.js
CHANGED
|
@@ -18,6 +18,9 @@ import { createInterface } from "node:readline/promises";
|
|
|
18
18
|
var REQUEST_TIMEOUT_MS$1 = 2e4;
|
|
19
19
|
/** Thrown when no browser tab is connected; the message is meant for the agent to relay. */
|
|
20
20
|
var NoBrowserError = class extends Error {};
|
|
21
|
+
function noBrowserError(url) {
|
|
22
|
+
return new NoBrowserError(`No PoseRef browser tab is connected. Call open_poseref, or ask the user to open ${url} in a browser, then retry. If that page does not load, PoseRef is not running: the user can start it with \`npx poseref\`.`);
|
|
23
|
+
}
|
|
21
24
|
/**
|
|
22
25
|
* Tracks the PoseRef browser tabs connected over WebSocket and forwards agent
|
|
23
26
|
* requests to one of them. The tab that most recently gained focus wins, so
|
|
@@ -27,9 +30,9 @@ var BrowserBridge = class {
|
|
|
27
30
|
tabs = [];
|
|
28
31
|
pending = /* @__PURE__ */ new Map();
|
|
29
32
|
nextId = 1;
|
|
30
|
-
|
|
31
|
-
constructor(
|
|
32
|
-
this.
|
|
33
|
+
opener;
|
|
34
|
+
constructor(opener) {
|
|
35
|
+
this.opener = opener;
|
|
33
36
|
}
|
|
34
37
|
get connectedTabs() {
|
|
35
38
|
return this.tabs.length;
|
|
@@ -40,9 +43,16 @@ var BrowserBridge = class {
|
|
|
40
43
|
socket.on("close", () => this.detach(socket));
|
|
41
44
|
socket.on("error", () => socket.close());
|
|
42
45
|
}
|
|
46
|
+
async open(automatic) {
|
|
47
|
+
return {
|
|
48
|
+
...await this.opener.ensureTab(automatic, async () => this.tabs.length),
|
|
49
|
+
url: this.opener.url,
|
|
50
|
+
startedServer: false
|
|
51
|
+
};
|
|
52
|
+
}
|
|
43
53
|
request(method, params) {
|
|
44
54
|
const socket = this.tabs[this.tabs.length - 1];
|
|
45
|
-
if (!socket) return Promise.reject(
|
|
55
|
+
if (!socket) return Promise.reject(noBrowserError(this.opener.url));
|
|
46
56
|
const id = this.nextId++;
|
|
47
57
|
const message = {
|
|
48
58
|
type: "request",
|
|
@@ -1837,6 +1847,7 @@ var DEFAULT_VIEW_SIZE = 480;
|
|
|
1837
1847
|
/** A contact within 2 cm reads as touching in the render. */
|
|
1838
1848
|
var REACHED_WITHIN = .02;
|
|
1839
1849
|
var INSTRUCTIONS = `PoseRef controls a posable mannequin and camera in the user's browser to build pose and composition reference images.
|
|
1850
|
+
The tools open the PoseRef page in the browser by themselves when no tab is connected. When the user asks to open or show PoseRef, call open_poseref.
|
|
1840
1851
|
Call get_pose_spec once before your first set_scene: it explains the bone axes and sign conventions, which are easy to get wrong.
|
|
1841
1852
|
Before posing, decide which way the character acts (a target, a direction) and keep that direction across the picture so the action reads in silhouette; aiming, throwing and swinging are done side-on (see the spec's "Composition and readability").
|
|
1842
1853
|
Then loop: set_scene -> render_views -> compare with the request and check the silhouette from the 'current' view -> adjust. When the request has body parts touching (hand on hip, elbow on knee, hands together), set the rough pose first and then place the contacts with reach (inverse kinematics) instead of guessing angles; confirm with get_joint_positions and only report contact the numbers show. set_scene puts the body on the floor automatically. Stop after two or three rounds when the pose is roughly right; the user fine-tunes by hand and can undo your changes in the app.`;
|
|
@@ -1857,6 +1868,20 @@ function compactScene(scene) {
|
|
|
1857
1868
|
}
|
|
1858
1869
|
});
|
|
1859
1870
|
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Runs a browser request. When no tab is connected, opens the page (the bridge
|
|
1873
|
+
* rate-limits automatic opening) and tries once more, so a closed tab does not
|
|
1874
|
+
* leave the agent stuck asking the user.
|
|
1875
|
+
*/
|
|
1876
|
+
async function withTab(bridge, call) {
|
|
1877
|
+
try {
|
|
1878
|
+
return await call();
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
if (!(error instanceof NoBrowserError)) throw error;
|
|
1881
|
+
if ((await bridge.open(true)).tabs === 0) throw error;
|
|
1882
|
+
return await call();
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1860
1885
|
function failure(error) {
|
|
1861
1886
|
return {
|
|
1862
1887
|
content: [{
|
|
@@ -1895,7 +1920,7 @@ function createPoseRefMcpServer(bridge) {
|
|
|
1895
1920
|
annotations: { readOnlyHint: true }
|
|
1896
1921
|
}, async () => {
|
|
1897
1922
|
try {
|
|
1898
|
-
return text(compactScene(await bridge.request("getScene", {})));
|
|
1923
|
+
return text(compactScene(await withTab(bridge, () => bridge.request("getScene", {}))));
|
|
1899
1924
|
} catch (error) {
|
|
1900
1925
|
return failure(error);
|
|
1901
1926
|
}
|
|
@@ -1906,10 +1931,10 @@ function createPoseRefMcpServer(bridge) {
|
|
|
1906
1931
|
inputSchema: sceneSchema
|
|
1907
1932
|
}, async (scene) => {
|
|
1908
1933
|
try {
|
|
1909
|
-
const { scene: applied, groundShift } = await bridge.request("setScene", {
|
|
1934
|
+
const { scene: applied, groundShift } = await withTab(bridge, () => bridge.request("setScene", {
|
|
1910
1935
|
version: 1,
|
|
1911
1936
|
...scene
|
|
1912
|
-
});
|
|
1937
|
+
}));
|
|
1913
1938
|
return text(`Applied.${groundShift === null ? "" : groundShift === 0 ? " Already resting on the floor." : ` Grounded: hips moved ${groundShift > 0 ? "up" : "down"} by ${Math.abs(groundShift)} m so the lowest body point rests on the floor.`} Current scene (bones not listed are 0): ${compactScene(applied)}`);
|
|
1914
1939
|
} catch (error) {
|
|
1915
1940
|
return failure(error);
|
|
@@ -1922,10 +1947,10 @@ function createPoseRefMcpServer(bridge) {
|
|
|
1922
1947
|
annotations: { readOnlyHint: true }
|
|
1923
1948
|
}, async ({ views, size }) => {
|
|
1924
1949
|
try {
|
|
1925
|
-
const result = await bridge.request("renderViews", {
|
|
1950
|
+
const result = await withTab(bridge, () => bridge.request("renderViews", {
|
|
1926
1951
|
views: views ?? [...DEFAULT_VIEWS],
|
|
1927
1952
|
size: size ?? DEFAULT_VIEW_SIZE
|
|
1928
|
-
});
|
|
1953
|
+
}));
|
|
1929
1954
|
const path = await saveRender(result.pngBase64);
|
|
1930
1955
|
return { content: [{
|
|
1931
1956
|
type: "image",
|
|
@@ -1946,7 +1971,7 @@ function createPoseRefMcpServer(bridge) {
|
|
|
1946
1971
|
annotations: { readOnlyHint: true }
|
|
1947
1972
|
}, async ({ pairs }) => {
|
|
1948
1973
|
try {
|
|
1949
|
-
const report = await bridge.request("getJoints", {});
|
|
1974
|
+
const report = await withTab(bridge, () => bridge.request("getJoints", {}));
|
|
1950
1975
|
const distances = (pairs ?? []).map(([a, b]) => {
|
|
1951
1976
|
const p = report.joints[a];
|
|
1952
1977
|
const q = report.joints[b];
|
|
@@ -1972,7 +1997,7 @@ function createPoseRefMcpServer(bridge) {
|
|
|
1972
1997
|
inputSchema: reachSchema
|
|
1973
1998
|
}, async ({ goals, ground }) => {
|
|
1974
1999
|
try {
|
|
1975
|
-
const scene = await bridge.request("getScene", {});
|
|
2000
|
+
const scene = await withTab(bridge, () => bridge.request("getScene", {}));
|
|
1976
2001
|
const frame = {
|
|
1977
2002
|
bones: scene.pose.bones,
|
|
1978
2003
|
rootOffset: scene.pose.rootOffset,
|
|
@@ -1986,14 +2011,14 @@ function createPoseRefMcpServer(bridge) {
|
|
|
1986
2011
|
}
|
|
1987
2012
|
const result = solveIk(frame, goals, chains);
|
|
1988
2013
|
const bones = Object.fromEntries(result.changed.map((bone) => [bone, result.bones[bone]]));
|
|
1989
|
-
const { groundShift } = await bridge.request("setScene", {
|
|
2014
|
+
const { groundShift } = await withTab(bridge, () => bridge.request("setScene", {
|
|
1990
2015
|
version: 1,
|
|
1991
2016
|
pose: {
|
|
1992
2017
|
mode: "merge",
|
|
1993
2018
|
bones,
|
|
1994
2019
|
ground
|
|
1995
2020
|
}
|
|
1996
|
-
});
|
|
2021
|
+
}));
|
|
1997
2022
|
const lines = result.goals.map((goal) => `- ${goal.effector}: ${goal.error <= REACHED_WITHIN ? "reached" : "NOT reached"}, ${goal.error} m from the target (rotated ${goal.chain.join(", ")})`);
|
|
1998
2023
|
const advice = result.goals.some((goal) => goal.error > REACHED_WITHIN) ? "\nA target out of reach leaves the limb pointing at it but short. Move the body closer first (lean the chest, bend the legs), add a parent bone such as chest to that goal's chain, or add a goal that moves the other body part toward this one." : "";
|
|
1999
2024
|
const grounding = groundShift ? `\nGrounded: hips moved ${groundShift > 0 ? "up" : "down"} by ${Math.abs(groundShift)} m.` : "";
|
|
@@ -2002,6 +2027,27 @@ function createPoseRefMcpServer(bridge) {
|
|
|
2002
2027
|
return failure(error);
|
|
2003
2028
|
}
|
|
2004
2029
|
});
|
|
2030
|
+
server.registerTool("open_poseref", {
|
|
2031
|
+
title: "Open PoseRef",
|
|
2032
|
+
description: "Starts PoseRef if it is not running and opens it in the user's browser. Does nothing when a PoseRef tab is already open. Use it when the user asks to open or show PoseRef; the other tools also open the page on their own when no tab is connected.",
|
|
2033
|
+
annotations: {
|
|
2034
|
+
readOnlyHint: false,
|
|
2035
|
+
destructiveHint: false,
|
|
2036
|
+
idempotentHint: true,
|
|
2037
|
+
openWorldHint: false
|
|
2038
|
+
}
|
|
2039
|
+
}, async () => {
|
|
2040
|
+
try {
|
|
2041
|
+
const result = await bridge.open(false);
|
|
2042
|
+
const started = result.startedServer ? "Started the PoseRef server. " : "";
|
|
2043
|
+
if (result.tabs > 0 && !result.opened) return text(`${started}PoseRef is already open at ${result.url} (${result.tabs} tab${result.tabs === 1 ? "" : "s"}).`);
|
|
2044
|
+
if (result.tabs > 0) return text(`${started}Opened PoseRef at ${result.url}.`);
|
|
2045
|
+
if (result.disabled) return text(`${started}Did not open a browser because POSEREF_NO_OPEN is set. Ask the user to open ${result.url}.`);
|
|
2046
|
+
return text(`${started}Opened ${result.url}, but the page has not connected yet. Ask the user to check the browser window.`);
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
return failure(error);
|
|
2049
|
+
}
|
|
2050
|
+
});
|
|
2005
2051
|
return server;
|
|
2006
2052
|
}
|
|
2007
2053
|
/** Stateless Streamable HTTP handler: every request gets a fresh McpServer bound to the shared bridge. */
|
|
@@ -2009,6 +2055,79 @@ function createPoseRefMcpHandler(bridge) {
|
|
|
2009
2055
|
return createMcpHandler(() => createPoseRefMcpServer(bridge));
|
|
2010
2056
|
}
|
|
2011
2057
|
//#endregion
|
|
2058
|
+
//#region server/browser.ts
|
|
2059
|
+
/**
|
|
2060
|
+
* Opens the PoseRef page in the default browser. The URL is always one PoseRef
|
|
2061
|
+
* built itself (http://127.0.0.1:<port>, or http://localhost:<port> for the Vite
|
|
2062
|
+
* dev server), and no shell is involved, so nothing from outside can reach the
|
|
2063
|
+
* command line.
|
|
2064
|
+
*/
|
|
2065
|
+
function openBrowser(url, log) {
|
|
2066
|
+
if (!/^http:\/\/(?:127\.0\.0\.1|localhost):\d+\/?$/.test(url)) throw new Error(`Refusing to open unexpected URL: ${url}`);
|
|
2067
|
+
const [command, args] = process.platform === "win32" ? ["explorer.exe", [url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
2068
|
+
const child = spawn(command, args, {
|
|
2069
|
+
detached: true,
|
|
2070
|
+
stdio: "ignore",
|
|
2071
|
+
windowsHide: true
|
|
2072
|
+
});
|
|
2073
|
+
child.on("error", () => log(`ブラウザで ${url} を開いてください。`));
|
|
2074
|
+
child.unref();
|
|
2075
|
+
}
|
|
2076
|
+
//#endregion
|
|
2077
|
+
//#region server/opener.ts
|
|
2078
|
+
var TAB_WAIT_MS = 2e4;
|
|
2079
|
+
var POLL_MS$1 = 300;
|
|
2080
|
+
/**
|
|
2081
|
+
* Automatic opening (a tool found no tab) happens at most this often, so a user
|
|
2082
|
+
* who closes the tab on purpose is not handed a new one on every tool call.
|
|
2083
|
+
* An explicit open_poseref call is never throttled.
|
|
2084
|
+
*/
|
|
2085
|
+
var AUTO_OPEN_COOLDOWN_MS = 6e4;
|
|
2086
|
+
var sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2087
|
+
/** Decides whether to open the PoseRef page, opens it, and waits for the tab to connect. */
|
|
2088
|
+
var PageOpener = class {
|
|
2089
|
+
options;
|
|
2090
|
+
lastAutoOpen = Number.NEGATIVE_INFINITY;
|
|
2091
|
+
constructor(options) {
|
|
2092
|
+
this.options = options;
|
|
2093
|
+
}
|
|
2094
|
+
get url() {
|
|
2095
|
+
return this.options.url;
|
|
2096
|
+
}
|
|
2097
|
+
async ensureTab(automatic, countTabs) {
|
|
2098
|
+
let tabs = await countTabs();
|
|
2099
|
+
if (tabs > 0) return {
|
|
2100
|
+
tabs,
|
|
2101
|
+
opened: false,
|
|
2102
|
+
disabled: false
|
|
2103
|
+
};
|
|
2104
|
+
if (!this.options.enabled) return {
|
|
2105
|
+
tabs,
|
|
2106
|
+
opened: false,
|
|
2107
|
+
disabled: true
|
|
2108
|
+
};
|
|
2109
|
+
if (automatic) {
|
|
2110
|
+
if (Date.now() - this.lastAutoOpen < AUTO_OPEN_COOLDOWN_MS) return {
|
|
2111
|
+
tabs,
|
|
2112
|
+
opened: false,
|
|
2113
|
+
disabled: false
|
|
2114
|
+
};
|
|
2115
|
+
this.lastAutoOpen = Date.now();
|
|
2116
|
+
}
|
|
2117
|
+
(this.options.openPage ?? ((url) => openBrowser(url, this.options.log)))(this.options.url);
|
|
2118
|
+
const deadline = Date.now() + TAB_WAIT_MS;
|
|
2119
|
+
while (tabs === 0 && Date.now() < deadline) {
|
|
2120
|
+
await sleep$1(POLL_MS$1);
|
|
2121
|
+
tabs = await countTabs();
|
|
2122
|
+
}
|
|
2123
|
+
return {
|
|
2124
|
+
tabs,
|
|
2125
|
+
opened: true,
|
|
2126
|
+
disabled: false
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
//#endregion
|
|
2012
2131
|
//#region server/app.ts
|
|
2013
2132
|
var MCP_PATH = "/mcp";
|
|
2014
2133
|
/** Loopback only: the server drives the user's browser tab and must not be reachable from the network. */
|
|
@@ -2053,7 +2172,12 @@ function createPoseRefServer(options) {
|
|
|
2053
2172
|
`http://localhost:${options.port}`,
|
|
2054
2173
|
...options.extraOrigins ?? []
|
|
2055
2174
|
]);
|
|
2056
|
-
const bridge = new BrowserBridge(
|
|
2175
|
+
const bridge = new BrowserBridge(new PageOpener({
|
|
2176
|
+
url: options.pageUrl ?? appUrl,
|
|
2177
|
+
enabled: !process.env.POSEREF_NO_OPEN,
|
|
2178
|
+
log: (message) => process.stderr.write(`${message}\n`),
|
|
2179
|
+
openPage: options.openPage
|
|
2180
|
+
}));
|
|
2057
2181
|
const mcp = toNodeHandler(createPoseRefMcpHandler(bridge));
|
|
2058
2182
|
const staticRoot = options.staticDir ? resolve(options.staticDir) : null;
|
|
2059
2183
|
const validateHost = localhostHostValidation();
|
|
@@ -2113,24 +2237,6 @@ function createPoseRefServer(options) {
|
|
|
2113
2237
|
};
|
|
2114
2238
|
}
|
|
2115
2239
|
//#endregion
|
|
2116
|
-
//#region server/browser.ts
|
|
2117
|
-
/**
|
|
2118
|
-
* Opens the PoseRef page in the default browser. The URL is always one PoseRef
|
|
2119
|
-
* built itself (http://127.0.0.1:<port>), and no shell is involved, so nothing
|
|
2120
|
-
* from outside can reach the command line.
|
|
2121
|
-
*/
|
|
2122
|
-
function openBrowser(url, log) {
|
|
2123
|
-
if (!/^http:\/\/127\.0\.0\.1:\d+\/?$/.test(url)) throw new Error(`Refusing to open unexpected URL: ${url}`);
|
|
2124
|
-
const [command, args] = process.platform === "win32" ? ["explorer.exe", [url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
2125
|
-
const child = spawn(command, args, {
|
|
2126
|
-
detached: true,
|
|
2127
|
-
stdio: "ignore",
|
|
2128
|
-
windowsHide: true
|
|
2129
|
-
});
|
|
2130
|
-
child.on("error", () => log(`ブラウザで ${url} を開いてください。`));
|
|
2131
|
-
child.unref();
|
|
2132
|
-
}
|
|
2133
|
-
//#endregion
|
|
2134
2240
|
//#region server/port.ts
|
|
2135
2241
|
/** Fixed default so the MCP URL registered with agents stays valid across restarts. */
|
|
2136
2242
|
var DEFAULT_PORT = 47173;
|
|
@@ -2141,7 +2247,6 @@ function resolvePort() {
|
|
|
2141
2247
|
//#endregion
|
|
2142
2248
|
//#region server/remote.ts
|
|
2143
2249
|
var START_TIMEOUT_MS = 15e3;
|
|
2144
|
-
var TAB_WAIT_MS = 2e4;
|
|
2145
2250
|
var POLL_MS = 300;
|
|
2146
2251
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
2147
2252
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -2157,11 +2262,18 @@ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
2157
2262
|
var RemoteBridge = class {
|
|
2158
2263
|
options;
|
|
2159
2264
|
baseUrl;
|
|
2265
|
+
opener;
|
|
2160
2266
|
ready = null;
|
|
2161
|
-
|
|
2267
|
+
startedServer = false;
|
|
2162
2268
|
constructor(options) {
|
|
2163
2269
|
this.options = options;
|
|
2164
2270
|
this.baseUrl = `http://127.0.0.1:${options.port}`;
|
|
2271
|
+
this.opener = new PageOpener({
|
|
2272
|
+
url: this.baseUrl,
|
|
2273
|
+
enabled: options.openBrowser,
|
|
2274
|
+
log: options.log,
|
|
2275
|
+
openPage: options.openPage
|
|
2276
|
+
});
|
|
2165
2277
|
}
|
|
2166
2278
|
async request(method, params) {
|
|
2167
2279
|
try {
|
|
@@ -2174,6 +2286,21 @@ var RemoteBridge = class {
|
|
|
2174
2286
|
return await this.call(method, params);
|
|
2175
2287
|
}
|
|
2176
2288
|
}
|
|
2289
|
+
async open(automatic) {
|
|
2290
|
+
this.ready = null;
|
|
2291
|
+
await this.ensureReady();
|
|
2292
|
+
const result = await this.opener.ensureTab(automatic, async () => {
|
|
2293
|
+
const probe = await this.probe();
|
|
2294
|
+
return probe.kind === "poseref" ? probe.tabs : 0;
|
|
2295
|
+
});
|
|
2296
|
+
const startedServer = this.startedServer;
|
|
2297
|
+
this.startedServer = false;
|
|
2298
|
+
return {
|
|
2299
|
+
...result,
|
|
2300
|
+
url: this.baseUrl,
|
|
2301
|
+
startedServer
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2177
2304
|
ensureReady() {
|
|
2178
2305
|
this.ready ??= this.prepare().catch((error) => {
|
|
2179
2306
|
this.ready = null;
|
|
@@ -2181,21 +2308,18 @@ var RemoteBridge = class {
|
|
|
2181
2308
|
});
|
|
2182
2309
|
return this.ready;
|
|
2183
2310
|
}
|
|
2311
|
+
/** Makes sure the listener on the port is PoseRef, starting it if nothing is there. */
|
|
2184
2312
|
async prepare() {
|
|
2185
2313
|
let probe = await this.probe();
|
|
2186
2314
|
if (probe.kind === "foreign") throw this.foreignError();
|
|
2187
2315
|
if (probe.kind === "absent") {
|
|
2188
2316
|
this.options.log("Starting the PoseRef server…");
|
|
2189
2317
|
this.options.startServer();
|
|
2318
|
+
this.startedServer = true;
|
|
2190
2319
|
probe = await this.waitFor((next) => next.kind !== "absent", START_TIMEOUT_MS);
|
|
2191
2320
|
if (probe.kind === "foreign") throw this.foreignError();
|
|
2192
2321
|
if (probe.kind === "absent") throw new Error("The PoseRef server did not start. Run `npx poseref start` in a terminal to see why.");
|
|
2193
2322
|
}
|
|
2194
|
-
if (this.options.openBrowser && probe.kind === "poseref" && probe.tabs === 0 && !this.browserOpened) {
|
|
2195
|
-
this.browserOpened = true;
|
|
2196
|
-
openBrowser(this.baseUrl, this.options.log);
|
|
2197
|
-
await this.waitFor((next) => next.kind === "poseref" && next.tabs > 0, TAB_WAIT_MS);
|
|
2198
|
-
}
|
|
2199
2323
|
}
|
|
2200
2324
|
async waitFor(done, timeoutMs) {
|
|
2201
2325
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2247,6 +2371,7 @@ var RemoteBridge = class {
|
|
|
2247
2371
|
}
|
|
2248
2372
|
const body = await response.json().catch(() => ({}));
|
|
2249
2373
|
if (body.ok === true) return body.result;
|
|
2374
|
+
if (response.status === 503) throw noBrowserError(this.baseUrl);
|
|
2250
2375
|
if (body.ok === false && body.error) throw new Error(body.error);
|
|
2251
2376
|
throw new Error(`The PoseRef server rejected the request (HTTP ${response.status}${body.error ? `: ${body.error}` : ""}).`);
|
|
2252
2377
|
}
|
|
@@ -2272,8 +2397,15 @@ function spawnDetachedServer(cliPath, log) {
|
|
|
2272
2397
|
}
|
|
2273
2398
|
//#endregion
|
|
2274
2399
|
//#region server/setup.ts
|
|
2275
|
-
/**
|
|
2276
|
-
|
|
2400
|
+
/**
|
|
2401
|
+
* Tools that are not read-only: two change the scene and open_poseref opens a
|
|
2402
|
+
* browser tab. Codex asks before each call unless they are approved in config.toml.
|
|
2403
|
+
*/
|
|
2404
|
+
var WRITE_TOOLS = [
|
|
2405
|
+
"set_scene",
|
|
2406
|
+
"reach",
|
|
2407
|
+
"open_poseref"
|
|
2408
|
+
];
|
|
2277
2409
|
/**
|
|
2278
2410
|
* The command agents run to start PoseRef's stdio MCP server. The version is
|
|
2279
2411
|
* pinned so a later release (or a compromised one) never runs without the user
|
|
@@ -2308,7 +2440,7 @@ function appendCodexApprovals(toml, server = "poseref") {
|
|
|
2308
2440
|
};
|
|
2309
2441
|
const blocks = added.map((tool) => `[mcp_servers.${server}.tools.${tool}]\napproval_mode = "approve"\n`);
|
|
2310
2442
|
return {
|
|
2311
|
-
text: `${toml}${toml.length === 0 || toml.endsWith("\n\n") ? "" : toml.endsWith("\n") ? "\n" : "\n\n"}# Added by \`poseref setup\`: these tools only change the PoseRef tab
|
|
2443
|
+
text: `${toml}${toml.length === 0 || toml.endsWith("\n\n") ? "" : toml.endsWith("\n") ? "\n" : "\n\n"}# Added by \`poseref setup\`: these tools only change or open the PoseRef tab; scene changes can be undone there.
|
|
2312
2444
|
${blocks.join("\n")}`,
|
|
2313
2445
|
added: [...added]
|
|
2314
2446
|
};
|
|
@@ -2421,7 +2553,7 @@ function codexSteps(launch, env, log) {
|
|
|
2421
2553
|
if (!added.ok) throw new Error(`codex mcp add failed${exists ? REMOVED_NOTE : ""}:\n${added.output}`);
|
|
2422
2554
|
}
|
|
2423
2555
|
}, {
|
|
2424
|
-
describe: `Let ${WRITE_TOOLS.join("
|
|
2556
|
+
describe: `Let ${WRITE_TOOLS.join(", ")} run without a prompt each time: append approval settings to ${configPath} (a backup is kept next to it)`,
|
|
2425
2557
|
apply: () => {
|
|
2426
2558
|
const { text, added } = appendCodexApprovals(existsSync(configPath) ? readFileSync(configPath, "utf8") : "");
|
|
2427
2559
|
if (added.length === 0) return;
|