obsbot-mcp 0.7.0 → 0.9.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.
Files changed (56) hide show
  1. package/README.md +111 -20
  2. package/dist/build-info.json +5 -0
  3. package/dist/device/manager.d.ts +8 -0
  4. package/dist/device/manager.js +37 -4
  5. package/dist/device/manager.js.map +1 -1
  6. package/dist/geometry/aim.d.ts +24 -13
  7. package/dist/geometry/aim.js +26 -13
  8. package/dist/geometry/aim.js.map +1 -1
  9. package/dist/ipc/build-id.d.ts +25 -0
  10. package/dist/ipc/build-id.js +60 -0
  11. package/dist/ipc/build-id.js.map +1 -0
  12. package/dist/ipc/client.d.ts +36 -0
  13. package/dist/ipc/client.js +106 -4
  14. package/dist/ipc/client.js.map +1 -1
  15. package/dist/ipc/coordinator.d.ts +91 -12
  16. package/dist/ipc/coordinator.js +293 -57
  17. package/dist/ipc/coordinator.js.map +1 -1
  18. package/dist/ipc/gate.d.ts +14 -0
  19. package/dist/ipc/gate.js +45 -0
  20. package/dist/ipc/gate.js.map +1 -0
  21. package/dist/ipc/owner.d.ts +27 -1
  22. package/dist/ipc/owner.js +67 -4
  23. package/dist/ipc/owner.js.map +1 -1
  24. package/dist/ipc/protocol.d.ts +30 -0
  25. package/dist/ipc/protocol.js +40 -0
  26. package/dist/ipc/protocol.js.map +1 -1
  27. package/dist/ipc/rendezvous.d.ts +37 -11
  28. package/dist/ipc/rendezvous.js +143 -30
  29. package/dist/ipc/rendezvous.js.map +1 -1
  30. package/dist/mcp/server.js +40 -12
  31. package/dist/mcp/server.js.map +1 -1
  32. package/dist/tail2/api.d.ts +381 -0
  33. package/dist/tail2/api.js +547 -0
  34. package/dist/tail2/api.js.map +1 -0
  35. package/dist/tail2/mdns.d.ts +48 -0
  36. package/dist/tail2/mdns.js +216 -0
  37. package/dist/tail2/mdns.js.map +1 -0
  38. package/dist/tail2/move.d.ts +41 -0
  39. package/dist/tail2/move.js +79 -0
  40. package/dist/tail2/move.js.map +1 -0
  41. package/dist/tail2/registry.d.ts +100 -0
  42. package/dist/tail2/registry.js +200 -0
  43. package/dist/tail2/registry.js.map +1 -0
  44. package/dist/tail2/snapshot.d.ts +54 -0
  45. package/dist/tail2/snapshot.js +157 -0
  46. package/dist/tail2/snapshot.js.map +1 -0
  47. package/dist/tail2/tools.d.ts +7 -0
  48. package/dist/tail2/tools.js +772 -0
  49. package/dist/tail2/tools.js.map +1 -0
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
  53. package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
  54. package/native/prebuilt/linux-x64/obsbot-helper +0 -0
  55. package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
  56. package/package.json +4 -1
@@ -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;
@@ -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
@@ -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":"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"}
@@ -1,5 +1,19 @@
1
1
  import net from "node:net";
2
- /** Well-known rendezvous name → platform endpoint. */
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
- * 1. Try to listen on `path` → win the bind → OWNER.
15
- * 2. EADDRINUSE → someone's there → connect → CLIENT.
16
- * 3. Connect refused on POSIX → the endpoint file is STALE (owner crashed
17
- * without cleaning up); unlink it and retry the listen → OWNER. Windows
18
- * named pipes are kernel-refcounted and never go stale, so this branch is
19
- * POSIX-only.
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
- * The stale-then-retry can itself lose a race to a concurrent starter that
22
- * binds first; that just yields EADDRINUSE again, so we bounce back to the
23
- * client path. A small bounded retry keeps a burst of simultaneous starts
24
- * from spuriously failing.
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>;
@@ -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
- /** Well-known rendezvous name → platform endpoint. */
17
- export function rendezvousPath(name = "obsbot-mcp") {
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
- * 1. Try to listen on `path` → win the bind → OWNER.
26
- * 2. EADDRINUSE → someone's there → connect → CLIENT.
27
- * 3. Connect refused on POSIX → the endpoint file is STALE (owner crashed
28
- * without cleaning up); unlink it and retry the listen → OWNER. Windows
29
- * named pipes are kernel-refcounted and never go stale, so this branch is
30
- * POSIX-only.
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
- * The stale-then-retry can itself lose a race to a concurrent starter that
33
- * binds first; that just yields EADDRINUSE again, so we bounce back to the
34
- * client path. A small bounded retry keeps a burst of simultaneous starts
35
- * from spuriously failing.
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 = 3) {
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
- return { role: "client", socket: await connect(path) };
151
+ closeSync(openSync(lock, "wx")); // fails if it exists: that is the lock
152
+ break;
49
153
  }
50
154
  catch (e) {
51
- lastErr = e;
52
- const code = errno(e);
53
- const stale = code === "ECONNREFUSED" || code === "ENOENT";
54
- if (stale && process.platform !== "win32") {
55
- try {
56
- unlinkSync(path);
57
- }
58
- catch {
59
- // already gone, or someone else cleaned it — fine; loop and retry.
60
- }
61
- continue; // retry listen
62
- }
63
- throw e; // a real connect failure that isn't a stale endpoint
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":"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"}
1
+ {"version":3,"file":"rendezvous.js","sourceRoot":"","sources":["../../src/ipc/rendezvous.ts"],"names":[],"mappings":"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"}
@@ -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, serialize } from "../ipc/coordinator.js";
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
- const tools = createTools(mgr, capture, opts.debug ?? false);
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 dies. runLocal is the tool dispatch, serialize()-wrapped so
35
- // it is the single-camera lock — covering both this instance's own calls and
36
- // any forwarded from clients (the local path bypasses OwnerServer's queue). A
37
- // lone instance is simply the owner with no peers: it behaves exactly as
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 = serialize(async (name, args) => {
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 = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}