obsbot-mcp 0.6.3 → 0.8.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 +97 -20
- package/dist/build-info.json +5 -0
- package/dist/device/manager.d.ts +8 -0
- package/dist/device/manager.js +37 -4
- package/dist/device/manager.js.map +1 -1
- package/dist/geometry/aim.d.ts +24 -13
- package/dist/geometry/aim.js +26 -13
- package/dist/geometry/aim.js.map +1 -1
- package/dist/ipc/build-id.d.ts +25 -0
- package/dist/ipc/build-id.js +60 -0
- package/dist/ipc/build-id.js.map +1 -0
- package/dist/ipc/client.d.ts +36 -0
- package/dist/ipc/client.js +106 -4
- package/dist/ipc/client.js.map +1 -1
- package/dist/ipc/coordinator.d.ts +91 -12
- package/dist/ipc/coordinator.js +293 -57
- package/dist/ipc/coordinator.js.map +1 -1
- package/dist/ipc/gate.d.ts +14 -0
- package/dist/ipc/gate.js +45 -0
- package/dist/ipc/gate.js.map +1 -0
- package/dist/ipc/owner.d.ts +27 -1
- package/dist/ipc/owner.js +67 -4
- package/dist/ipc/owner.js.map +1 -1
- package/dist/ipc/protocol.d.ts +30 -0
- package/dist/ipc/protocol.js +40 -0
- package/dist/ipc/protocol.js.map +1 -1
- package/dist/ipc/rendezvous.d.ts +37 -11
- package/dist/ipc/rendezvous.js +143 -30
- package/dist/ipc/rendezvous.js.map +1 -1
- package/dist/mcp/server.js +40 -12
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +6 -0
- package/dist/mcp/tools.js +123 -17
- package/dist/mcp/tools.js.map +1 -1
- package/dist/tail2/api.d.ts +218 -0
- package/dist/tail2/api.js +267 -0
- package/dist/tail2/api.js.map +1 -0
- package/dist/tail2/mdns.d.ts +48 -0
- package/dist/tail2/mdns.js +216 -0
- package/dist/tail2/mdns.js.map +1 -0
- package/dist/tail2/registry.d.ts +100 -0
- package/dist/tail2/registry.js +200 -0
- package/dist/tail2/registry.js.map +1 -0
- package/dist/tail2/snapshot.d.ts +54 -0
- package/dist/tail2/snapshot.js +157 -0
- package/dist/tail2/snapshot.js.map +1 -0
- package/dist/tail2/tools.d.ts +7 -0
- package/dist/tail2/tools.js +370 -0
- package/dist/tail2/tools.js.map +1 -0
- package/dist/transport/helper-process.d.ts +11 -0
- package/dist/transport/helper-process.js +10 -0
- package/dist/transport/helper-process.js.map +1 -1
- package/dist/transport/linux.d.ts +59 -9
- package/dist/transport/linux.js +163 -21
- package/dist/transport/linux.js.map +1 -1
- package/dist/transport/macos.d.ts +8 -0
- package/dist/transport/macos.js +11 -0
- package/dist/transport/macos.js.map +1 -1
- package/dist/transport/transport.d.ts +22 -0
- package/dist/transport/transport.js +20 -0
- package/dist/transport/transport.js.map +1 -1
- package/dist/transport/windows.d.ts +6 -0
- package/dist/transport/windows.js +9 -0
- package/dist/transport/windows.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
- package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
- package/native/prebuilt/linux-x64/obsbot-helper +0 -0
- package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
- package/package.json +4 -1
package/dist/ipc/protocol.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BuildId } from "./build-id.js";
|
|
1
2
|
/** Upper bound on a single frame. Snapshots (base64 JPEG) are the large case. */
|
|
2
3
|
export declare const MAX_FRAME: number;
|
|
3
4
|
export interface RpcMessage {
|
|
@@ -19,3 +20,32 @@ export declare class FrameDecoder {
|
|
|
19
20
|
private buf;
|
|
20
21
|
push(chunk: Buffer): RpcMessage[];
|
|
21
22
|
}
|
|
23
|
+
/** Frame id of an owner→client notice. Request ids start at 1, so it never matches a reply. */
|
|
24
|
+
export declare const NOTICE_ID = 0;
|
|
25
|
+
export interface HelloBody {
|
|
26
|
+
ipc: "hello";
|
|
27
|
+
build: BuildId;
|
|
28
|
+
pid: number;
|
|
29
|
+
}
|
|
30
|
+
export interface TakeoverBody {
|
|
31
|
+
ipc: "takeover";
|
|
32
|
+
build: BuildId;
|
|
33
|
+
pid: number;
|
|
34
|
+
}
|
|
35
|
+
export interface SteppingDownBody {
|
|
36
|
+
ipc: "stepping-down";
|
|
37
|
+
successorPid: number;
|
|
38
|
+
}
|
|
39
|
+
export type TakeoverResult = {
|
|
40
|
+
ipc: "takeover";
|
|
41
|
+
granted: true;
|
|
42
|
+
} | {
|
|
43
|
+
ipc: "takeover";
|
|
44
|
+
granted: false;
|
|
45
|
+
reason: "not-newer";
|
|
46
|
+
};
|
|
47
|
+
/** The body as a hello, or undefined if it is not a well-formed one. */
|
|
48
|
+
export declare function asHello(body: unknown): HelloBody | undefined;
|
|
49
|
+
/** The body as a takeover request, or undefined if it is not a well-formed one. */
|
|
50
|
+
export declare function asTakeover(body: unknown): TakeoverBody | undefined;
|
|
51
|
+
export declare function isSteppingDown(body: unknown): body is SteppingDownBody;
|
package/dist/ipc/protocol.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isBuildId } from "./build-id.js";
|
|
1
2
|
// ---------------------------------------------------------------------------
|
|
2
3
|
// Length-prefixed JSON framing for the client↔owner control channel.
|
|
3
4
|
//
|
|
@@ -53,4 +54,43 @@ export class FrameDecoder {
|
|
|
53
54
|
return out;
|
|
54
55
|
}
|
|
55
56
|
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Control messages — how instances agree on who owns the endpoint.
|
|
59
|
+
//
|
|
60
|
+
// A tool request's body is {tool, args}. A control message is a body with a
|
|
61
|
+
// reserved `ipc` key, carried in the same frames. See
|
|
62
|
+
// docs/superpowers/specs/2026-09-27-newest-build-wins-design.md §6.
|
|
63
|
+
//
|
|
64
|
+
// An instance that predates these treats any body as a tool call, so a hello
|
|
65
|
+
// sent to it comes back as {ok:false, error:"unknown tool: undefined"} — which
|
|
66
|
+
// is how a legacy owner is recognised. A legacy CLIENT ignores a frame whose id
|
|
67
|
+
// it is not waiting for, so a notice sent with NOTICE_ID is harmless to it.
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
/** Frame id of an owner→client notice. Request ids start at 1, so it never matches a reply. */
|
|
70
|
+
export const NOTICE_ID = 0;
|
|
71
|
+
const isPid = (x) => typeof x === "number" && Number.isInteger(x) && x > 0;
|
|
72
|
+
function identityOf(body, kind) {
|
|
73
|
+
if (typeof body !== "object" || body === null)
|
|
74
|
+
return undefined;
|
|
75
|
+
const b = body;
|
|
76
|
+
if (b.ipc !== kind || !isBuildId(b.build) || !isPid(b.pid))
|
|
77
|
+
return undefined;
|
|
78
|
+
return { build: b.build, pid: b.pid };
|
|
79
|
+
}
|
|
80
|
+
/** The body as a hello, or undefined if it is not a well-formed one. */
|
|
81
|
+
export function asHello(body) {
|
|
82
|
+
const id = identityOf(body, "hello");
|
|
83
|
+
return id && { ipc: "hello", ...id };
|
|
84
|
+
}
|
|
85
|
+
/** The body as a takeover request, or undefined if it is not a well-formed one. */
|
|
86
|
+
export function asTakeover(body) {
|
|
87
|
+
const id = identityOf(body, "takeover");
|
|
88
|
+
return id && { ipc: "takeover", ...id };
|
|
89
|
+
}
|
|
90
|
+
export function isSteppingDown(body) {
|
|
91
|
+
if (typeof body !== "object" || body === null)
|
|
92
|
+
return false;
|
|
93
|
+
const b = body;
|
|
94
|
+
return b.ipc === "stepping-down" && isPid(b.successorPid);
|
|
95
|
+
}
|
|
56
96
|
//# sourceMappingURL=protocol.js.map
|
package/dist/ipc/protocol.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/ipc/protocol.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,qEAAqE;AACrE,EAAE;AACF,gFAAgF;AAChF,0EAA0E;AAC1E,8EAA8E;AAC9E,yEAAyE;AACzE,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,gDAAgD;AAChD,8EAA8E;AAE9E,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,+BAA+B;AACjD,iFAAiF;AACjF,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,SAAS;AAOpD,sFAAsF;AACtF,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACtD,IAAI,IAAI,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,CAAC,MAAM,QAAQ,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;IACf,GAAG,GAAW,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEtC,IAAI,CAAC,KAAa;QAChB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QAC5E,MAAM,GAAG,GAAiB,EAAE,CAAC;QAC7B,SAAS,CAAC;YACR,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM;gBAAE,MAAM,CAAC,oCAAoC;YACzE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACrC,IAAI,GAAG,GAAG,SAAS,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CAAC,mCAAmC,GAAG,QAAQ,CAAC,CAAC;YAClE,CAAC;YACD,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,GAAG,GAAG;gBAAE,MAAM,CAAC,yBAAyB;YACpE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,CAAC;YACrD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAe,CAAC,CAAC;YAC1D,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;CACF"}
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/ipc/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,eAAe,CAAC;AAExD,8EAA8E;AAC9E,qEAAqE;AACrE,EAAE;AACF,gFAAgF;AAChF,0EAA0E;AAC1E,8EAA8E;AAC9E,yEAAyE;AACzE,EAAE;AACF,0EAA0E;AAC1E,4EAA4E;AAC5E,gDAAgD;AAChD,8EAA8E;AAE9E,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,+BAA+B;AACjD,iFAAiF;AACjF,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,SAAS;AAOpD,sFAAsF;AACtF,MAAM,UAAU,WAAW,CAAC,GAAY;IACtC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IACtD,IAAI,IAAI,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,gCAAgC,IAAI,CAAC,MAAM,QAAQ,CAAC,CAAC;IACvE,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;IACf,GAAG,GAAW,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEtC,IAAI,CAAC,KAAa;QAChB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QAC5E,MAAM,GAAG,GAAiB,EAAE,CAAC;QAC7B,SAAS,CAAC;YACR,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM;gBAAE,MAAM,CAAC,oCAAoC;YACzE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACrC,IAAI,GAAG,GAAG,SAAS,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CAAC,mCAAmC,GAAG,QAAQ,CAAC,CAAC;YAClE,CAAC;YACD,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,GAAG,GAAG;gBAAE,MAAM,CAAC,yBAAyB;YACpE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,CAAC;YACrD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAe,CAAC,CAAC;YAC1D,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;CACF;AAED,8EAA8E;AAC9E,mEAAmE;AACnE,EAAE;AACF,4EAA4E;AAC5E,sDAAsD;AACtD,oEAAoE;AACpE,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,4EAA4E;AAC5E,8EAA8E;AAE9E,+FAA+F;AAC/F,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC;AAuB3B,MAAM,KAAK,GAAG,CAAC,CAAU,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEjG,SAAS,UAAU,CAAC,IAAa,EAAE,IAA0B;IAC3D,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAChE,MAAM,CAAC,GAAG,IAA+B,CAAC;IAC1C,IAAI,CAAC,CAAC,GAAG,KAAK,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7E,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;AACxC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,OAAO,CAAC,IAAa;IACnC,MAAM,EAAE,GAAG,UAAU,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACrC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;AACvC,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,IAAa;IACtC,MAAM,EAAE,GAAG,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IACxC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAa;IAC1C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC5D,MAAM,CAAC,GAAG,IAA+B,CAAC;IAC1C,OAAO,CAAC,CAAC,GAAG,KAAK,eAAe,IAAI,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;AAC5D,CAAC"}
|
package/dist/ipc/rendezvous.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The rendezvous name: OBSBOT_IPC_NAME, or the well-known default.
|
|
4
|
+
*
|
|
5
|
+
* Every instance that should share one camera owner must use the same name, so
|
|
6
|
+
* the default is right for normal use. The variable exists for test harnesses
|
|
7
|
+
* — which would otherwise take the endpoint away from a developer's live
|
|
8
|
+
* session — and for deliberately isolating one session on its own build.
|
|
9
|
+
*
|
|
10
|
+
* The name becomes part of a filesystem path or pipe name, so anything outside
|
|
11
|
+
* a conservative character set is refused rather than sanitised. Unset and
|
|
12
|
+
* empty both mean the default: an MCP client config that declares the variable
|
|
13
|
+
* with no value should not be a startup error.
|
|
14
|
+
*/
|
|
15
|
+
export declare function rendezvousName(env?: NodeJS.ProcessEnv): string;
|
|
16
|
+
/** Rendezvous name → platform endpoint. */
|
|
3
17
|
export declare function rendezvousPath(name?: string): string;
|
|
4
18
|
export type Role = {
|
|
5
19
|
role: "owner";
|
|
@@ -11,16 +25,28 @@ export type Role = {
|
|
|
11
25
|
/**
|
|
12
26
|
* Become the owner, or attach as a client.
|
|
13
27
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
28
|
+
* The bind is the lock: exactly one process can hold the name. What needs care
|
|
29
|
+
* is the name that is held by NOBODY — the socket file an owner leaves behind
|
|
30
|
+
* when it is killed outright (POSIX only; a Windows pipe is kernel-refcounted
|
|
31
|
+
* and vanishes with its owner).
|
|
32
|
+
*
|
|
33
|
+
* Clearing a stale file means unlinking it, and unlink cannot tell a stale
|
|
34
|
+
* file from a live one. If two callers both find the name taken and the
|
|
35
|
+
* connection refused, and each unlinks and binds, the second unlinks the
|
|
36
|
+
* FIRST one's live socket: two owners, and the first never finds out. That is
|
|
37
|
+
* no longer a corner case — every client of a killed owner re-elects at once.
|
|
38
|
+
*
|
|
39
|
+
* So on POSIX the unlink, and every bind, happens under a lock file taken with
|
|
40
|
+
* an exclusive create:
|
|
41
|
+
*
|
|
42
|
+
* 1. Connect. Someone answers → CLIENT.
|
|
43
|
+
* 2. Nobody answers → take the lock, and look again: another caller may have
|
|
44
|
+
* bound while we waited. Someone answers → CLIENT.
|
|
45
|
+
* 3. Still nobody → unlink whatever is there, bind → OWNER.
|
|
20
46
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
47
|
+
* On Windows nothing is ever stale and nothing is unlinked, so there is no
|
|
48
|
+
* lock: bind → OWNER, else connect → CLIENT. A name that is taken and then
|
|
49
|
+
* refuses the connection means the owner went away in between, which is a
|
|
50
|
+
* reason to go round again, not to fail.
|
|
25
51
|
*/
|
|
26
52
|
export declare function elect(path?: string, attempts?: number): Promise<Role>;
|
package/dist/ipc/rendezvous.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
|
-
import { unlinkSync } from "node:fs";
|
|
2
|
+
import { closeSync, openSync, statSync, unlinkSync } from "node:fs";
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// Peer election over a well-known local endpoint.
|
|
5
5
|
//
|
|
@@ -7,14 +7,39 @@ import { unlinkSync } from "node:fs";
|
|
|
7
7
|
// bind and becomes the OWNER (it will run the single DeviceManager + native
|
|
8
8
|
// helper and serve everyone else); the rest get EADDRINUSE and attach as
|
|
9
9
|
// CLIENTS that forward their helper ops to the owner. The bind is the lock —
|
|
10
|
-
// there is no check-then-create step to race (see IPC-DESIGN.md).
|
|
10
|
+
// there is no check-then-create step to race (see IPC-DESIGN.md). Clearing a
|
|
11
|
+
// name that nobody holds is the one step that is not atomic; see elect().
|
|
11
12
|
//
|
|
12
13
|
// Transport is a named pipe (Windows) / Unix-domain socket (macOS, Linux),
|
|
13
14
|
// NOT shared memory: it gives atomic election, framing, wakeup, and clean
|
|
14
15
|
// crash-detection for free.
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
const DEFAULT_NAME = "obsbot-mcp";
|
|
18
|
+
const NAME_RE = /^[A-Za-z0-9._-]{1,64}$/;
|
|
19
|
+
/**
|
|
20
|
+
* The rendezvous name: OBSBOT_IPC_NAME, or the well-known default.
|
|
21
|
+
*
|
|
22
|
+
* Every instance that should share one camera owner must use the same name, so
|
|
23
|
+
* the default is right for normal use. The variable exists for test harnesses
|
|
24
|
+
* — which would otherwise take the endpoint away from a developer's live
|
|
25
|
+
* session — and for deliberately isolating one session on its own build.
|
|
26
|
+
*
|
|
27
|
+
* The name becomes part of a filesystem path or pipe name, so anything outside
|
|
28
|
+
* a conservative character set is refused rather than sanitised. Unset and
|
|
29
|
+
* empty both mean the default: an MCP client config that declares the variable
|
|
30
|
+
* with no value should not be a startup error.
|
|
31
|
+
*/
|
|
32
|
+
export function rendezvousName(env = process.env) {
|
|
33
|
+
const raw = env.OBSBOT_IPC_NAME;
|
|
34
|
+
if (raw === undefined || raw === "")
|
|
35
|
+
return DEFAULT_NAME;
|
|
36
|
+
if (!NAME_RE.test(raw)) {
|
|
37
|
+
throw new Error(`OBSBOT_IPC_NAME must be 1-64 characters from A-Z a-z 0-9 . _ - (got ${JSON.stringify(raw)})`);
|
|
38
|
+
}
|
|
39
|
+
return raw;
|
|
40
|
+
}
|
|
41
|
+
/** Rendezvous name → platform endpoint. */
|
|
42
|
+
export function rendezvousPath(name = rendezvousName()) {
|
|
18
43
|
return process.platform === "win32"
|
|
19
44
|
? `\\\\.\\pipe\\${name}`
|
|
20
45
|
: `/tmp/${name}.sock`; // portable across macOS + Linux; abstract sockets are Linux-only
|
|
@@ -22,48 +47,136 @@ export function rendezvousPath(name = "obsbot-mcp") {
|
|
|
22
47
|
/**
|
|
23
48
|
* Become the owner, or attach as a client.
|
|
24
49
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
50
|
+
* The bind is the lock: exactly one process can hold the name. What needs care
|
|
51
|
+
* is the name that is held by NOBODY — the socket file an owner leaves behind
|
|
52
|
+
* when it is killed outright (POSIX only; a Windows pipe is kernel-refcounted
|
|
53
|
+
* and vanishes with its owner).
|
|
54
|
+
*
|
|
55
|
+
* Clearing a stale file means unlinking it, and unlink cannot tell a stale
|
|
56
|
+
* file from a live one. If two callers both find the name taken and the
|
|
57
|
+
* connection refused, and each unlinks and binds, the second unlinks the
|
|
58
|
+
* FIRST one's live socket: two owners, and the first never finds out. That is
|
|
59
|
+
* no longer a corner case — every client of a killed owner re-elects at once.
|
|
60
|
+
*
|
|
61
|
+
* So on POSIX the unlink, and every bind, happens under a lock file taken with
|
|
62
|
+
* an exclusive create:
|
|
31
63
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
64
|
+
* 1. Connect. Someone answers → CLIENT.
|
|
65
|
+
* 2. Nobody answers → take the lock, and look again: another caller may have
|
|
66
|
+
* bound while we waited. Someone answers → CLIENT.
|
|
67
|
+
* 3. Still nobody → unlink whatever is there, bind → OWNER.
|
|
68
|
+
*
|
|
69
|
+
* On Windows nothing is ever stale and nothing is unlinked, so there is no
|
|
70
|
+
* lock: bind → OWNER, else connect → CLIENT. A name that is taken and then
|
|
71
|
+
* refuses the connection means the owner went away in between, which is a
|
|
72
|
+
* reason to go round again, not to fail.
|
|
36
73
|
*/
|
|
37
|
-
export async function elect(path = rendezvousPath(), attempts =
|
|
74
|
+
export async function elect(path = rendezvousPath(), attempts = 5) {
|
|
38
75
|
let lastErr;
|
|
39
76
|
for (let i = 0; i < attempts; i++) {
|
|
77
|
+
const r = process.platform === "win32" ? await electPipe(path) : await electSocket(path);
|
|
78
|
+
if (r.role !== "retry")
|
|
79
|
+
return r;
|
|
80
|
+
lastErr = r.because;
|
|
81
|
+
await sleep(RETRY_MS);
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`elect: could not become owner or client after ${attempts} attempts: ${errno(lastErr) ?? lastErr}`);
|
|
84
|
+
}
|
|
85
|
+
const RETRY_MS = 10;
|
|
86
|
+
/** A lock older than this was left by a holder that died. The lock is held for milliseconds. */
|
|
87
|
+
const LOCK_STALE_MS = 5000;
|
|
88
|
+
/** Longest one caller waits for the lock. */
|
|
89
|
+
const LOCK_WAIT_MS = 3000;
|
|
90
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
91
|
+
const nobodyThere = (e) => {
|
|
92
|
+
const code = errno(e);
|
|
93
|
+
return code === "ECONNREFUSED" || code === "ENOENT";
|
|
94
|
+
};
|
|
95
|
+
async function electPipe(path) {
|
|
96
|
+
try {
|
|
97
|
+
return { role: "owner", server: await listen(path) };
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
if (errno(e) !== "EADDRINUSE")
|
|
101
|
+
throw e;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return { role: "client", socket: await connect(path) };
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (!nobodyThere(e))
|
|
108
|
+
throw e;
|
|
109
|
+
return { role: "retry", because: e };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function electSocket(path) {
|
|
113
|
+
try {
|
|
114
|
+
return { role: "client", socket: await connect(path) };
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
if (!nobodyThere(e))
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
120
|
+
return withLock(`${path}.lock`, async () => {
|
|
121
|
+
try {
|
|
122
|
+
return { role: "client", socket: await connect(path) }; // someone bound while we waited
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
if (!nobodyThere(e))
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
unlinkSync(path);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// nothing there to clear
|
|
133
|
+
}
|
|
40
134
|
try {
|
|
41
135
|
return { role: "owner", server: await listen(path) };
|
|
42
136
|
}
|
|
43
137
|
catch (e) {
|
|
138
|
+
// An instance that predates the lock can still bind between our unlink
|
|
139
|
+
// and our bind. It is there now; go round and connect to it.
|
|
44
140
|
if (errno(e) !== "EADDRINUSE")
|
|
45
141
|
throw e;
|
|
142
|
+
return { role: "retry", because: e };
|
|
46
143
|
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/** Run `fn` holding `lock`, a file taken with an exclusive create and removed afterwards. */
|
|
147
|
+
async function withLock(lock, fn) {
|
|
148
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
149
|
+
for (;;) {
|
|
47
150
|
try {
|
|
48
|
-
|
|
151
|
+
closeSync(openSync(lock, "wx")); // fails if it exists: that is the lock
|
|
152
|
+
break;
|
|
49
153
|
}
|
|
50
154
|
catch (e) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
155
|
+
if (errno(e) !== "EEXIST")
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS)
|
|
160
|
+
unlinkSync(lock);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// released, or cleared by someone else, since we looked
|
|
164
|
+
}
|
|
165
|
+
if (Date.now() > deadline)
|
|
166
|
+
throw new Error(`elect: could not take ${lock} in ${LOCK_WAIT_MS} ms`);
|
|
167
|
+
await sleep(5 + Math.random() * 10);
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
return await fn();
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
try {
|
|
174
|
+
unlinkSync(lock);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// already gone
|
|
64
178
|
}
|
|
65
179
|
}
|
|
66
|
-
throw new Error(`elect: could not become owner or client after ${attempts} attempts: ${errno(lastErr) ?? lastErr}`);
|
|
67
180
|
}
|
|
68
181
|
function listen(path) {
|
|
69
182
|
return new Promise((resolve, reject) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rendezvous.js","sourceRoot":"","sources":["../../src/ipc/rendezvous.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"rendezvous.js","sourceRoot":"","sources":["../../src/ipc/rendezvous.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/server.js
CHANGED
|
@@ -6,11 +6,19 @@ import { DeviceManager } from "../device/manager.js";
|
|
|
6
6
|
import { helperFactory } from "../device/helper-factory.js";
|
|
7
7
|
import { makeLogSink } from "./log-sink.js";
|
|
8
8
|
import { createTools } from "./tools.js";
|
|
9
|
+
import { Tail2Registry } from "../tail2/registry.js";
|
|
10
|
+
import { createTail2Tools } from "../tail2/tools.js";
|
|
9
11
|
import { renderToolResult } from "./render.js";
|
|
10
12
|
import { CaptureManager } from "../capture/manager.js";
|
|
11
|
-
import { Coordinator
|
|
13
|
+
import { Coordinator } from "../ipc/coordinator.js";
|
|
14
|
+
import { buildLabel, loadBuildId } from "../ipc/build-id.js";
|
|
12
15
|
import { VERSION } from "../version.js";
|
|
13
16
|
export async function startServer(opts = {}) {
|
|
17
|
+
// Which build this process loaded. Read first and never again: a rebuild
|
|
18
|
+
// under a running process changes the stamp file but not the process, and
|
|
19
|
+
// the identity has to describe the process. See src/ipc/build-id.ts.
|
|
20
|
+
const build = loadBuildId();
|
|
21
|
+
console.error(`obsbot-mcp: build ${buildLabel(build)}`);
|
|
14
22
|
// helperFactory subscribes every helper it spawns to the OS bus events, so
|
|
15
23
|
// the manager hears about a camera arriving or leaving instead of finding
|
|
16
24
|
// out by failing a call. See src/device/helper-factory.ts.
|
|
@@ -27,27 +35,47 @@ export async function startServer(opts = {}) {
|
|
|
27
35
|
// `camera` selector — so nothing per-camera is wired up out here anymore.
|
|
28
36
|
const capture = new CaptureManager();
|
|
29
37
|
// --debug exposes the RE/diagnostics surface (obsbot_debug_probe tool + status raw block).
|
|
30
|
-
|
|
38
|
+
// Tail 2 tools are pure TypeScript (HTTP/WS — no native helper), so they ride
|
|
39
|
+
// along on every platform identically; see TAIL2-PROTOCOL.md. The registry
|
|
40
|
+
// seeds itself from OBSBOT_TAIL2_HOSTS and holds no persistent resources in
|
|
41
|
+
// this first increment (status reads are one-shot WebSockets), so there is
|
|
42
|
+
// nothing of its own to close in `shutdown`.
|
|
43
|
+
const tail2 = Tail2Registry.fromEnv(process.env);
|
|
44
|
+
const tools = [
|
|
45
|
+
...createTools(mgr, capture, opts.debug ?? false),
|
|
46
|
+
...createTail2Tools(tail2),
|
|
47
|
+
];
|
|
31
48
|
// Single-owner camera coordination across concurrent MCP clients (see
|
|
32
49
|
// IPC-DESIGN.md). Every instance elects: the owner runs tool calls locally
|
|
33
50
|
// against the one DeviceManager; clients forward theirs to the owner and
|
|
34
|
-
// re-elect if it
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
51
|
+
// re-elect if it goes away. The owner is the newest build alive — an older
|
|
52
|
+
// owner steps down for a newer one (see
|
|
53
|
+
// docs/superpowers/specs/2026-09-27-newest-build-wins-design.md). runLocal is
|
|
54
|
+
// the bare tool dispatch; the coordinator runs it behind the single-camera
|
|
55
|
+
// lock, which covers this instance's own calls and the ones forwarded to it.
|
|
56
|
+
// A lone instance is simply the owner with no peers: it behaves exactly as
|
|
38
57
|
// before, plus an idle listener.
|
|
39
|
-
const runLocal =
|
|
58
|
+
const runLocal = async (name, args) => {
|
|
40
59
|
const tool = tools.find((t) => t.name === name);
|
|
41
60
|
if (!tool)
|
|
42
61
|
throw new Error(`unknown tool: ${name}`);
|
|
43
62
|
return tool.handler(args);
|
|
63
|
+
};
|
|
64
|
+
const coordinator = new Coordinator(runLocal, {
|
|
65
|
+
build,
|
|
66
|
+
// The coordinator reports its role on every change, on STDERR (never
|
|
67
|
+
// stdout — that's the JSON-RPC channel). "Which build will run my next
|
|
68
|
+
// call" is answered by the last `ipc role=` line; the harnesses match on it.
|
|
69
|
+
log: (line) => console.error(line),
|
|
70
|
+
// What stepping down for a newer build has to let go of: the same things a
|
|
71
|
+
// clean exit does, without the exit. On macOS the control open is
|
|
72
|
+
// exclusive, so the successor cannot open the camera until this is done.
|
|
73
|
+
release: async () => {
|
|
74
|
+
capture.stopAll();
|
|
75
|
+
await mgr.shutdown();
|
|
76
|
+
},
|
|
44
77
|
});
|
|
45
|
-
const coordinator = new Coordinator(runLocal);
|
|
46
78
|
await coordinator.start();
|
|
47
|
-
// Report the coordination role on STDERR (never stdout — that's the JSON-RPC
|
|
48
|
-
// channel). Useful for ops ("am I the owner or a client?") and observed by the
|
|
49
|
-
// ipc-hw-smoke harness to confirm a client really forwards to the owner.
|
|
50
|
-
console.error(`obsbot-mcp: ipc role=${coordinator.roleName}`);
|
|
51
79
|
// Kill any recording/preview child processes when the server exits, so nothing
|
|
52
80
|
// orphans, and drop the IPC endpoint / owner connection.
|
|
53
81
|
const shutdown = () => {
|
package/dist/mcp/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAW,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/tools.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export interface PresetReadOpts {
|
|
|
55
55
|
attempts?: number;
|
|
56
56
|
backoffMs?: number[];
|
|
57
57
|
}
|
|
58
|
+
export interface PoseSettleOpts {
|
|
59
|
+
pollMs?: number;
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
/** Beat between the two pose reads that decide whether the gimbal is moving. */
|
|
62
|
+
motionPollMs?: number;
|
|
63
|
+
}
|
|
58
64
|
export interface ZoomSettleOpts {
|
|
59
65
|
pollMs?: number;
|
|
60
66
|
timeoutMs?: number;
|