obsbot-mcp 0.3.1 → 0.4.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 (69) hide show
  1. package/README.md +197 -38
  2. package/dist/codec/commands.d.ts +42 -4
  3. package/dist/codec/commands.js +72 -10
  4. package/dist/codec/commands.js.map +1 -1
  5. package/dist/codec/frame.d.ts +1 -0
  6. package/dist/codec/frame.js +1 -1
  7. package/dist/codec/frame.js.map +1 -1
  8. package/dist/codec/preset.d.ts +40 -0
  9. package/dist/codec/preset.js +198 -0
  10. package/dist/codec/preset.js.map +1 -0
  11. package/dist/codec/types.d.ts +4 -0
  12. package/dist/device/helper-factory.d.ts +25 -0
  13. package/dist/device/helper-factory.js +40 -0
  14. package/dist/device/helper-factory.js.map +1 -0
  15. package/dist/device/manager.d.ts +326 -2
  16. package/dist/device/manager.js +720 -24
  17. package/dist/device/manager.js.map +1 -1
  18. package/dist/ipc/client.d.ts +19 -0
  19. package/dist/ipc/client.js +81 -0
  20. package/dist/ipc/client.js.map +1 -0
  21. package/dist/ipc/coordinator.d.ts +29 -0
  22. package/dist/ipc/coordinator.js +94 -0
  23. package/dist/ipc/coordinator.js.map +1 -0
  24. package/dist/ipc/owner.d.ts +21 -0
  25. package/dist/ipc/owner.js +59 -0
  26. package/dist/ipc/owner.js.map +1 -0
  27. package/dist/ipc/protocol.d.ts +21 -0
  28. package/dist/ipc/protocol.js +56 -0
  29. package/dist/ipc/protocol.js.map +1 -0
  30. package/dist/ipc/rendezvous.d.ts +26 -0
  31. package/dist/ipc/rendezvous.js +93 -0
  32. package/dist/ipc/rendezvous.js.map +1 -0
  33. package/dist/mcp/log-sink.d.ts +12 -0
  34. package/dist/mcp/log-sink.js +27 -0
  35. package/dist/mcp/log-sink.js.map +1 -0
  36. package/dist/mcp/ready.d.ts +16 -4
  37. package/dist/mcp/ready.js +11 -8
  38. package/dist/mcp/ready.js.map +1 -1
  39. package/dist/mcp/render.js +1 -1
  40. package/dist/mcp/render.js.map +1 -1
  41. package/dist/mcp/server.js +68 -18
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/mcp/tools.d.ts +5 -3
  44. package/dist/mcp/tools.js +787 -186
  45. package/dist/mcp/tools.js.map +1 -1
  46. package/dist/transport/helper-process.d.ts +36 -0
  47. package/dist/transport/helper-process.js +190 -10
  48. package/dist/transport/helper-process.js.map +1 -1
  49. package/dist/transport/linux.d.ts +40 -0
  50. package/dist/transport/linux.js +92 -2
  51. package/dist/transport/linux.js.map +1 -1
  52. package/dist/transport/macos.d.ts +13 -0
  53. package/dist/transport/macos.js +59 -3
  54. package/dist/transport/macos.js.map +1 -1
  55. package/dist/transport/read-serial.d.ts +26 -0
  56. package/dist/transport/read-serial.js +87 -0
  57. package/dist/transport/read-serial.js.map +1 -0
  58. package/dist/transport/transport.d.ts +24 -0
  59. package/dist/transport/windows.d.ts +4 -0
  60. package/dist/transport/windows.js +31 -4
  61. package/dist/transport/windows.js.map +1 -1
  62. package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
  63. package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
  64. package/native/prebuilt/linux-x64/obsbot-helper +0 -0
  65. package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
  66. package/package.json +3 -2
  67. package/dist/device/session.d.ts +0 -22
  68. package/dist/device/session.js +0 -37
  69. package/dist/device/session.js.map +0 -1
@@ -1,5 +1,16 @@
1
1
  import { ObsbotTransport } from "../transport/transport.js";
2
- import { DeviceSession } from "../device/session.js";
2
+ /**
3
+ * The minimal slice of connection-lifecycle control the readiness gate needs to
4
+ * self-heal a mid-session disconnect: drop the dead transport so a re-resolve
5
+ * rebinds, and report (once, clearing on read) whether that re-resolve was a
6
+ * reconnect. Satisfied by DeviceManager (via a thin adapter in server.ts); the
7
+ * two methods are exactly what the retired DeviceSession exposed, so ensureReady
8
+ * changes only the type it depends on, not its body.
9
+ */
10
+ export interface ReconnectCtl {
11
+ invalidate(): Promise<void>;
12
+ takeReconnected(): boolean;
13
+ }
3
14
  export type ReadyResult = {
4
15
  ok: true;
5
16
  transport: ObsbotTransport;
@@ -14,6 +25,7 @@ export interface ReadyOpts {
14
25
  wakeTimeoutMs?: number;
15
26
  settleMs?: number;
16
27
  }
28
+ export declare const msg: (e: unknown) => string;
17
29
  /**
18
30
  * Gate a gimbal/AI command on device readiness. A single status read is a 3-way
19
31
  * probe: it throws when the device is unreachable, and otherwise reports awake.
@@ -25,7 +37,7 @@ export interface ReadyOpts {
25
37
  * - awake → { ok:true, transport, reconnected }.
26
38
  *
27
39
  * The command is only sent by the caller after ok:true — a gate failure never
28
- * touches the gimbal. Without a session, self-heal is skipped but awake-gating
29
- * still applies.
40
+ * touches the gimbal. Without a reconnect controller, self-heal is skipped but
41
+ * awake-gating still applies.
30
42
  */
31
- export declare function ensureReady(getTransport: () => Promise<ObsbotTransport>, session?: DeviceSession, opts?: ReadyOpts): Promise<ReadyResult>;
43
+ export declare function ensureReady(getTransport: () => Promise<ObsbotTransport>, reconnect?: ReconnectCtl, opts?: ReadyOpts): Promise<ReadyResult>;
package/dist/mcp/ready.js CHANGED
@@ -2,7 +2,10 @@ import { decodeStatus } from "../codec/commands.js";
2
2
  import { encodeSetRunStatus } from "../codec/commands.js";
3
3
  const DEFAULTS = { pollMs: 200, wakeTimeoutMs: 2500, settleMs: 300 };
4
4
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5
- const msg = (e) => (e instanceof Error ? e.message : String(e));
5
+ // Exported for reuse by any caller catch block that needs a safe error string —
6
+ // `(e as Error).message` yields "undefined" on a non-Error throw (e.g. a string
7
+ // or plain object thrown by a mock/dependency).
8
+ export const msg = (e) => (e instanceof Error ? e.message : String(e));
6
9
  const readAwake = async (t) => decodeStatus(await t.recvStatus()).awake;
7
10
  /**
8
11
  * Gate a gimbal/AI command on device readiness. A single status read is a 3-way
@@ -15,10 +18,10 @@ const readAwake = async (t) => decodeStatus(await t.recvStatus()).awake;
15
18
  * - awake → { ok:true, transport, reconnected }.
16
19
  *
17
20
  * The command is only sent by the caller after ok:true — a gate failure never
18
- * touches the gimbal. Without a session, self-heal is skipped but awake-gating
19
- * still applies.
21
+ * touches the gimbal. Without a reconnect controller, self-heal is skipped but
22
+ * awake-gating still applies.
20
23
  */
21
- export async function ensureReady(getTransport, session, opts = {}) {
24
+ export async function ensureReady(getTransport, reconnect, opts = {}) {
22
25
  const { pollMs, wakeTimeoutMs, settleMs } = { ...DEFAULTS, ...opts };
23
26
  let t;
24
27
  try {
@@ -32,11 +35,11 @@ export async function ensureReady(getTransport, session, opts = {}) {
32
35
  awake = await readAwake(t);
33
36
  }
34
37
  catch (e) {
35
- // Probe failed — device likely unplugged. Self-heal once if we have a session.
36
- if (!session) {
38
+ // Probe failed — device likely unplugged. Self-heal once if we can.
39
+ if (!reconnect) {
37
40
  return { ok: false, reason: "unreachable", error: `camera not reachable: ${msg(e)}` };
38
41
  }
39
- session.invalidate();
42
+ await reconnect.invalidate();
40
43
  try {
41
44
  t = await getTransport();
42
45
  awake = await readAwake(t);
@@ -66,6 +69,6 @@ export async function ensureReady(getTransport, session, opts = {}) {
66
69
  }
67
70
  await sleep(settleMs); // let the gimbal finish rising before we drive it
68
71
  }
69
- return { ok: true, transport: t, reconnected: session?.takeReconnected() ?? false };
72
+ return { ok: true, transport: t, reconnected: reconnect?.takeReconnected() ?? false };
70
73
  }
71
74
  //# sourceMappingURL=ready.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ready.js","sourceRoot":"","sources":["../../src/mcp/ready.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAc1D,MAAM,QAAQ,GAAwB,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;AAE1F,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AACnF,MAAM,GAAG,GAAG,CAAC,CAAU,EAAU,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAEjF,MAAM,SAAS,GAAG,KAAK,EAAE,CAAkB,EAAoB,EAAE,CAC/D,YAAY,CAAC,MAAM,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC;AAE3C;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,YAA4C,EAC5C,OAAuB,EACvB,OAAkB,EAAE;IAEpB,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC;IAErE,IAAI,CAAkB,CAAC;IACvB,IAAI,CAAC;QACH,CAAC,GAAG,MAAM,YAAY,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,qBAAqB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;IACpF,CAAC;IAED,IAAI,KAAc,CAAC;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,+EAA+E;QAC/E,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,yBAAyB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACxF,CAAC;QACD,OAAO,CAAC,UAAU,EAAE,CAAC;QACrB,IAAI,CAAC;YACH,CAAC,GAAG,MAAM,YAAY,EAAE,CAAC;YACzB,KAAK,GAAG,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,EAAE,EAAE,CAAC;YACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,yBAAyB,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACzF,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,CAAC,CAAC,UAAU,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACtE,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,OAAO,MAAM,GAAG,aAAa,EAAE,CAAC;YAC9B,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC;YACpB,MAAM,IAAI,MAAM,CAAC;YACjB,IAAI,CAAC;gBACH,IAAI,MAAM,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;oBACvB,KAAK,GAAG,IAAI,CAAC;oBACb,MAAM;gBACR,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,+DAA+D;YACjE,CAAC;QACH,CAAC;QACD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC;QAC5F,CAAC;QACD,MAAM,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,kDAAkD;IAC3E,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,KAAK,EAAE,CAAC;AACtF,CAAC"}
1
+ {"version":3,"file":"ready.js","sourceRoot":"","sources":["../../src/mcp/ready.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  // Render a tool handler's return value into an MCP CallTool result. A handler
2
2
  // may either return a plain object (serialized to a single text block, the
3
3
  // default for every control tool) or an object that already carries an MCP
4
- // `content` array (e.g. obsbot_snapshot returning an image block), which is
4
+ // `content` array (e.g. obsbot_capture_snapshot returning an image block), which is
5
5
  // passed through untouched.
6
6
  export function renderToolResult(result) {
7
7
  if (result !== null &&
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/mcp/render.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,4EAA4E;AAC5E,4BAA4B;AAC5B,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,IACE,MAAM,KAAK,IAAI;QACf,OAAO,MAAM,KAAK,QAAQ;QAC1B,KAAK,CAAC,OAAO,CAAE,MAAgC,CAAC,OAAO,CAAC,EACxD,CAAC;QACD,OAAO,MAAgC,CAAC;IAC1C,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;AACvE,CAAC"}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/mcp/render.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,2EAA2E;AAC3E,2EAA2E;AAC3E,oFAAoF;AACpF,4BAA4B;AAC5B,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,IACE,MAAM,KAAK,IAAI;QACf,OAAO,MAAM,KAAK,QAAQ;QAC1B,KAAK,CAAC,OAAO,CAAE,MAAgC,CAAC,OAAO,CAAC,EACxD,CAAC;QACD,OAAO,MAAgC,CAAC;IAC1C,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;AACvE,CAAC"}
@@ -3,28 +3,75 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
4
  import { zodToJsonSchema } from "zod-to-json-schema";
5
5
  import { DeviceManager } from "../device/manager.js";
6
- import { DeviceSession } from "../device/session.js";
7
- import { HelperProcess } from "../transport/helper-process.js";
6
+ import { helperFactory } from "../device/helper-factory.js";
7
+ import { makeLogSink } from "./log-sink.js";
8
8
  import { createTools } from "./tools.js";
9
9
  import { renderToolResult } from "./render.js";
10
10
  import { CaptureManager } from "../capture/manager.js";
11
+ import { Coordinator, serialize } from "../ipc/coordinator.js";
11
12
  export async function startServer(opts = {}) {
12
- const helper = new HelperProcess();
13
- await helper.start();
14
- const mgr = new DeviceManager(helper);
15
- // DeviceSession owns the connection lifecycle: lazy open, invalidate + re-open
16
- // on a mid-session disconnect (self-heal), and reconnect tracking.
17
- const session = new DeviceSession(mgr);
18
- const getTransport = () => session.get();
13
+ // helperFactory subscribes every helper it spawns to the OS bus events, so
14
+ // the manager hears about a camera arriving or leaving instead of finding
15
+ // out by failing a call. See src/device/helper-factory.ts.
16
+ //
17
+ // OBSBOT_LOG_FILE additionally mirrors the manager's diagnostics to a file:
18
+ // our stderr goes to a pipe the launcher owns, so a rare event like a
19
+ // retried arrival re-bind would otherwise be unobservable after the fact.
20
+ const log = makeLogSink(process.env.OBSBOT_LOG_FILE, (m) => console.error(m));
21
+ const mgr = new DeviceManager(helperFactory(() => mgr), { log });
22
+ // DeviceManager now owns the connection lifecycle: lazy bind, invalidate +
23
+ // re-bind on a mid-session disconnect (self-heal), and per-camera reconnect
24
+ // tracking. createTools derives per-camera resolution (getTransport / reconnect
25
+ // / readiness gate) from the manager itself, keyed by each tool's optional
26
+ // `camera` selector — so nothing per-camera is wired up out here anymore.
19
27
  const capture = new CaptureManager();
20
- // --debug exposes the RE/diagnostics surface (obsbot_probe tool + get_status raw block).
21
- const tools = createTools(getTransport, mgr, capture, session, opts.debug ?? false);
22
- // Kill any recording/preview child processes when the server exits, so nothing orphans.
23
- const shutdown = () => capture.stopAll();
28
+ // --debug exposes the RE/diagnostics surface (obsbot_debug_probe tool + status raw block).
29
+ const tools = createTools(mgr, capture, opts.debug ?? false);
30
+ // Single-owner camera coordination across concurrent MCP clients (see
31
+ // IPC-DESIGN.md). Every instance elects: the owner runs tool calls locally
32
+ // against the one DeviceManager; clients forward theirs to the owner and
33
+ // re-elect if it dies. runLocal is the tool dispatch, serialize()-wrapped so
34
+ // it is the single-camera lock — covering both this instance's own calls and
35
+ // any forwarded from clients (the local path bypasses OwnerServer's queue). A
36
+ // lone instance is simply the owner with no peers: it behaves exactly as
37
+ // before, plus an idle listener.
38
+ const runLocal = serialize(async (name, args) => {
39
+ const tool = tools.find((t) => t.name === name);
40
+ if (!tool)
41
+ throw new Error(`unknown tool: ${name}`);
42
+ return tool.handler(args);
43
+ });
44
+ const coordinator = new Coordinator(runLocal);
45
+ await coordinator.start();
46
+ // Report the coordination role on STDERR (never stdout — that's the JSON-RPC
47
+ // channel). Useful for ops ("am I the owner or a client?") and observed by the
48
+ // ipc-hw-smoke harness to confirm a client really forwards to the owner.
49
+ console.error(`obsbot-mcp: ipc role=${coordinator.roleName}`);
50
+ // Kill any recording/preview child processes when the server exits, so nothing
51
+ // orphans, and drop the IPC endpoint / owner connection.
52
+ const shutdown = () => {
53
+ capture.stopAll();
54
+ void coordinator.close();
55
+ // The manager holds a watcher process that is deliberately never closed by
56
+ // any operation, so nothing else would ever stop it.
57
+ //
58
+ // Belt-and-braces, and measured to be so: with this line commented out, a
59
+ // real server bound to hardware and sent SIGTERM still left zero orphaned
60
+ // helpers, because they exit on stdin EOF when we die (helper.m:1180). The
61
+ // SIGINT/SIGTERM handlers also process.exit(0) immediately after this, and
62
+ // it is fire-and-forget, so the close() very likely does not finish.
63
+ //
64
+ // Kept anyway: it costs nothing, it is the correct thing to call, and it
65
+ // covers an orderly stop that does NOT tear down our stdio. Do not mistake
66
+ // it for what reaps the helpers — that is the pipe.
67
+ void mgr.shutdown();
68
+ };
24
69
  process.on("exit", shutdown);
25
70
  process.on("SIGINT", () => { shutdown(); process.exit(0); });
26
71
  process.on("SIGTERM", () => { shutdown(); process.exit(0); });
27
- const server = new Server({ name: "obsbot-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
72
+ const server = new Server(
73
+ // Must match package.json's version; test/version-sync.test.ts enforces it.
74
+ { name: "obsbot-mcp", version: "0.4.0" }, { capabilities: { tools: {} } });
28
75
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
29
76
  tools: tools.map((tool) => ({
30
77
  name: tool.name,
@@ -36,11 +83,14 @@ export async function startServer(opts = {}) {
36
83
  })),
37
84
  }));
38
85
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
39
- const tool = tools.find((t) => t.name === request.params.name);
40
- if (!tool) {
41
- throw new Error(`unknown tool: ${request.params.name}`);
86
+ const name = request.params.name;
87
+ if (!tools.find((t) => t.name === name)) {
88
+ throw new Error(`unknown tool: ${name}`);
42
89
  }
43
- const result = await tool.handler(request.params.arguments ?? {});
90
+ // Route through the coordinator: run locally if we own the camera, else
91
+ // forward to whoever does. renderToolResult runs here at the MCP boundary,
92
+ // on the raw tool result either way.
93
+ const result = await coordinator.dispatch(name, request.params.arguments ?? {});
44
94
  return renderToolResult(result);
45
95
  });
46
96
  const stdioTransport = new StdioServerTransport();
@@ -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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"}
@@ -1,7 +1,5 @@
1
1
  import { z } from "zod";
2
- import { ObsbotTransport } from "../transport/transport.js";
3
2
  import { DeviceManager } from "../device/manager.js";
4
- import { DeviceSession } from "../device/session.js";
5
3
  import type { CaptureManager } from "../capture/manager.js";
6
4
  export interface ToolDef {
7
5
  name: string;
@@ -9,4 +7,8 @@ export interface ToolDef {
9
7
  schema: z.ZodTypeAny;
10
8
  handler: (args: unknown) => Promise<object>;
11
9
  }
12
- export declare function createTools(getTransport: () => Promise<ObsbotTransport>, mgr: DeviceManager, capture?: CaptureManager, session?: DeviceSession, debug?: boolean): ToolDef[];
10
+ export interface PresetReadOpts {
11
+ attempts?: number;
12
+ backoffMs?: number[];
13
+ }
14
+ export declare function createTools(mgr: DeviceManager, capture?: CaptureManager, debug?: boolean, presetRead?: PresetReadOpts): ToolDef[];