File size: 6,139 Bytes
e18726e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion";
import type { Command } from "commander";
import type { callGatewayFromCli } from "../cli/gateway-rpc.js";
import type { MeetingPluginConfig } from "./plugin-config.js";

type MeetingCliMode = "agent" | "bidi" | "transcribe";
type MeetingCliTransport = "chrome" | "chrome-node";

type MeetingCliDescriptions = {
  root: string;
  join: string;
  leave: string;
  status: string;
  setup: string;
  testSpeech: string;
  testListen: string;
};

type MeetingCliOptions = {
  callGateway: typeof callGatewayFromCli;
  commandName: string;
  config: MeetingPluginConfig;
  descriptions: MeetingCliDescriptions;
  methodPrefix: string;
  program: Command;
  resolveGatewayTimeoutMs(params: {
    config: MeetingPluginConfig;
    method: string;
    requestedTimeoutMs?: number;
  }): number;
};

type JoinOptions = {
  transport?: string;
  mode?: string;
  message?: string;
  timeoutMs?: string;
};

function parseTimeout(value: string | undefined): number | undefined {
  if (value === undefined) {
    return undefined;
  }
  const parsed = parseStrictNonNegativeInteger(value);
  if (parsed === undefined || parsed === 0) {
    throw new Error("timeout-ms must be a positive integer");
  }
  return parsed;
}

function parseMeetingCliMode(value: string | undefined): MeetingCliMode | undefined {
  if (value === undefined || value === "agent" || value === "bidi" || value === "transcribe") {
    return value;
  }
  throw new Error(`mode must be agent, bidi, or transcribe; received ${value}`);
}

function parseMeetingCliTransport(value: string | undefined): MeetingCliTransport | undefined {
  if (value === undefined || value === "chrome" || value === "chrome-node") {
    return value;
  }
  throw new Error(`transport must be chrome or chrome-node; received ${value}`);
}

function markMeetingSetupFailure(result: unknown): void {
  if (result && typeof result === "object" && "ok" in result && result.ok === false) {
    process.exitCode = 1;
  }
}

function joinPayload(url: string, options: JoinOptions): Record<string, unknown> {
  return {
    url,
    ...(options.transport ? { transport: parseMeetingCliTransport(options.transport) } : {}),
    ...(options.mode ? { mode: parseMeetingCliMode(options.mode) } : {}),
    ...(options.message ? { message: options.message } : {}),
    ...(options.timeoutMs ? { timeoutMs: parseTimeout(options.timeoutMs) } : {}),
  };
}

function addJoinOptions(command: Command): Command {
  return command
    .option("--transport <transport>", "chrome or chrome-node")
    .option("--mode <mode>", "agent, bidi, or transcribe")
    .option("--message <text>", "instructions to speak after joining");
}

export function registerMeetingPluginCli(options: MeetingCliOptions): void {
  const call = async (method: string, payload?: Record<string, unknown>) => {
    const requestedTimeoutMs =
      typeof payload?.timeoutMs === "number" ? payload.timeoutMs : undefined;
    const timeoutMs = options.resolveGatewayTimeoutMs({
      config: options.config,
      method,
      requestedTimeoutMs,
    });
    const result = await options.callGateway(
      method,
      { json: true, timeout: String(timeoutMs) },
      payload,
      { progress: false, scopes: ["operator.admin"] },
    );
    process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
    return result;
  };
  const method = (action: string) => `${options.methodPrefix}.${action}`;
  const root = options.program.command(options.commandName).description(options.descriptions.root);
  const command = (usage: string, description: string) =>
    root.command(usage).description(description);

  addJoinOptions(command("join <url>", options.descriptions.join)).action(
    async (url: string, joinOptions: JoinOptions) => {
      await call(method("join"), joinPayload(url, joinOptions));
    },
  );
  command("leave <session-id>", options.descriptions.leave).action(async (sessionId: string) => {
    await call(method("leave"), { sessionId });
  });
  command("status [session-id]", options.descriptions.status).action(async (sessionId?: string) => {
    await call(method("status"), sessionId ? { sessionId } : {});
  });
  command("transcript <session-id>", "read the current transcript snapshot")
    .option("--since-index <index>", "resume from a prior transcript index")
    .action(async (sessionId: string, transcriptOptions: { sinceIndex?: string }) => {
      const sinceIndex =
        transcriptOptions.sinceIndex === undefined
          ? undefined
          : parseStrictNonNegativeInteger(transcriptOptions.sinceIndex);
      if (transcriptOptions.sinceIndex !== undefined && sinceIndex === undefined) {
        throw new Error("since-index must be a non-negative integer");
      }
      await call(method("transcript"), {
        sessionId,
        ...(sinceIndex === undefined ? {} : { sinceIndex }),
      });
    });
  command("speak <session-id> [message]", "speak through an active talk-back session").action(
    async (sessionId: string, message?: string) => {
      await call(method("speak"), { sessionId, ...(message ? { message } : {}) });
    },
  );
  command("setup", options.descriptions.setup)
    .option("--transport <transport>", "chrome or chrome-node")
    .option("--mode <mode>", "agent, bidi, or transcribe")
    .action(async (setupOptions: { transport?: string; mode?: string }) => {
      const result = await call(method("setup"), {
        transport: parseMeetingCliTransport(setupOptions.transport),
        mode: parseMeetingCliMode(setupOptions.mode),
      });
      markMeetingSetupFailure(result);
    });

  for (const [name, action, description] of [
    ["test-speech", "testSpeech", options.descriptions.testSpeech],
    ["test-listen", "testListen", options.descriptions.testListen],
  ] as const) {
    addJoinOptions(command(`${name} <url>`, description))
      .option("--timeout-ms <ms>", "probe timeout in milliseconds")
      .action(async (url: string, joinOptions: JoinOptions) => {
        await call(method(action), joinPayload(url, joinOptions));
      });
  }
}