Gateway: add PTT chat + nodes CLI
parent
1a48bce294
commit
b7aac92ac4
|
|
@ -75,6 +75,7 @@ Text + native (when enabled):
|
||||||
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
|
- `/usage off|tokens|full|cost` (per-response usage footer or local cost summary)
|
||||||
- `/tts off|always|inbound|tagged|status|provider|limit|summary|audio` (control TTS; see [/tts](/tts))
|
- `/tts off|always|inbound|tagged|status|provider|limit|summary|audio` (control TTS; see [/tts](/tts))
|
||||||
- Discord: native command is `/voice` (Discord reserves `/tts`); text `/tts` still works.
|
- Discord: native command is `/voice` (Discord reserves `/tts`); text `/tts` still works.
|
||||||
|
- `/ptt start|stop|once|cancel [node=<id>]` (push-to-talk controls for a paired node)
|
||||||
- `/stop`
|
- `/stop`
|
||||||
- `/restart`
|
- `/restart`
|
||||||
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
|
- `/dock-telegram` (alias: `/dock_telegram`) (switch replies to Telegram)
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,28 @@ function buildChatCommands(): ChatCommandDefinition[] {
|
||||||
],
|
],
|
||||||
argsMenu: "auto",
|
argsMenu: "auto",
|
||||||
}),
|
}),
|
||||||
|
defineChatCommand({
|
||||||
|
key: "ptt",
|
||||||
|
nativeName: "ptt",
|
||||||
|
description: "Push-to-talk controls for a paired node.",
|
||||||
|
textAlias: "/ptt",
|
||||||
|
acceptsArgs: true,
|
||||||
|
argsParsing: "none",
|
||||||
|
category: "tools",
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: "action",
|
||||||
|
description: "start, stop, once, or cancel",
|
||||||
|
type: "string",
|
||||||
|
choices: ["start", "stop", "once", "cancel"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "node",
|
||||||
|
description: "node=<id> (optional)",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
defineChatCommand({
|
defineChatCommand({
|
||||||
key: "config",
|
key: "config",
|
||||||
nativeName: "config",
|
nativeName: "config",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ import {
|
||||||
} from "./commands-info.js";
|
} from "./commands-info.js";
|
||||||
import { handleModelsCommand } from "./commands-models.js";
|
import { handleModelsCommand } from "./commands-models.js";
|
||||||
import { handlePluginCommand } from "./commands-plugin.js";
|
import { handlePluginCommand } from "./commands-plugin.js";
|
||||||
|
import { handlePTTCommand } from "./commands-ptt.js";
|
||||||
|
import { handleTtsCommands } from "./commands-tts.js";
|
||||||
import {
|
import {
|
||||||
handleAbortTrigger,
|
handleAbortTrigger,
|
||||||
handleActivationCommand,
|
handleActivationCommand,
|
||||||
|
|
@ -30,7 +32,6 @@ import {
|
||||||
handleUsageCommand,
|
handleUsageCommand,
|
||||||
} from "./commands-session.js";
|
} from "./commands-session.js";
|
||||||
import { handleSubagentsCommand } from "./commands-subagents.js";
|
import { handleSubagentsCommand } from "./commands-subagents.js";
|
||||||
import { handleTtsCommands } from "./commands-tts.js";
|
|
||||||
import { routeReply } from "./route-reply.js";
|
import { routeReply } from "./route-reply.js";
|
||||||
|
|
||||||
let HANDLERS: CommandHandler[] | null = null;
|
let HANDLERS: CommandHandler[] | null = null;
|
||||||
|
|
@ -46,6 +47,7 @@ export async function handleCommands(params: HandleCommandsParams): Promise<Comm
|
||||||
handleUsageCommand,
|
handleUsageCommand,
|
||||||
handleRestartCommand,
|
handleRestartCommand,
|
||||||
handleTtsCommands,
|
handleTtsCommands,
|
||||||
|
handlePTTCommand,
|
||||||
handleHelpCommand,
|
handleHelpCommand,
|
||||||
handleCommandsListCommand,
|
handleCommandsListCommand,
|
||||||
handleStatusCommand,
|
handleStatusCommand,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import type { MsgContext } from "../templating.js";
|
||||||
|
import { buildCommandContext, handleCommands } from "./commands.js";
|
||||||
|
import { parseInlineDirectives } from "./directive-handling.js";
|
||||||
|
|
||||||
|
const callGateway = vi.fn(async (_opts: { method?: string }) => ({ ok: true }));
|
||||||
|
|
||||||
|
vi.mock("../../gateway/call.js", () => ({
|
||||||
|
callGateway: (opts: unknown) => callGateway(opts as { method?: string }),
|
||||||
|
randomIdempotencyKey: () => "idem-test",
|
||||||
|
}));
|
||||||
|
|
||||||
|
function buildParams(commandBody: string, cfg: OpenClawConfig, ctxOverrides?: Partial<MsgContext>) {
|
||||||
|
const ctx = {
|
||||||
|
Body: commandBody,
|
||||||
|
CommandBody: commandBody,
|
||||||
|
CommandSource: "text",
|
||||||
|
CommandAuthorized: true,
|
||||||
|
Provider: "telegram",
|
||||||
|
Surface: "telegram",
|
||||||
|
...ctxOverrides,
|
||||||
|
} as MsgContext;
|
||||||
|
|
||||||
|
const command = buildCommandContext({
|
||||||
|
ctx,
|
||||||
|
cfg,
|
||||||
|
isGroup: false,
|
||||||
|
triggerBodyNormalized: commandBody.trim().toLowerCase(),
|
||||||
|
commandAuthorized: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
ctx,
|
||||||
|
cfg,
|
||||||
|
command,
|
||||||
|
directives: parseInlineDirectives(commandBody),
|
||||||
|
elevated: { enabled: true, allowed: true, failures: [] },
|
||||||
|
sessionKey: "agent:main:main",
|
||||||
|
workspaceDir: "/tmp",
|
||||||
|
defaultGroupActivation: () => "mention",
|
||||||
|
resolvedVerboseLevel: "off" as const,
|
||||||
|
resolvedReasoningLevel: "off" as const,
|
||||||
|
resolveDefaultThinkingLevel: async () => undefined,
|
||||||
|
provider: "telegram",
|
||||||
|
model: "test-model",
|
||||||
|
contextTokens: 0,
|
||||||
|
isGroup: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("handleCommands /ptt", () => {
|
||||||
|
it("invokes talk.ptt.once on the default iOS node", async () => {
|
||||||
|
callGateway.mockImplementation(async (opts: { method?: string; params?: unknown }) => {
|
||||||
|
if (opts.method === "node.list") {
|
||||||
|
return {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
nodeId: "ios-1",
|
||||||
|
displayName: "iPhone",
|
||||||
|
platform: "ios",
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (opts.method === "node.invoke") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
nodeId: "ios-1",
|
||||||
|
command: "talk.ptt.once",
|
||||||
|
payload: { status: "offline" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
|
const cfg = {
|
||||||
|
commands: { text: true },
|
||||||
|
channels: { telegram: { allowFrom: ["*"] } },
|
||||||
|
} as OpenClawConfig;
|
||||||
|
const params = buildParams("/ptt once", cfg);
|
||||||
|
const result = await handleCommands(params);
|
||||||
|
|
||||||
|
expect(result.shouldContinue).toBe(false);
|
||||||
|
expect(result.reply?.text).toContain("PTT once");
|
||||||
|
expect(result.reply?.text).toContain("status: offline");
|
||||||
|
|
||||||
|
const invokeCall = callGateway.mock.calls.find((call) => call[0]?.method === "node.invoke");
|
||||||
|
expect(invokeCall).toBeTruthy();
|
||||||
|
expect(invokeCall?.[0]?.params?.command).toBe("talk.ptt.once");
|
||||||
|
expect(invokeCall?.[0]?.params?.idempotencyKey).toBe("idem-test");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { logVerbose } from "../../globals.js";
|
||||||
|
import { callGateway, randomIdempotencyKey } from "../../gateway/call.js";
|
||||||
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import type { CommandHandler } from "./commands-types.js";
|
||||||
|
|
||||||
|
type NodeSummary = {
|
||||||
|
nodeId: string;
|
||||||
|
displayName?: string;
|
||||||
|
platform?: string;
|
||||||
|
deviceFamily?: string;
|
||||||
|
remoteIp?: string;
|
||||||
|
connected?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PTT_COMMANDS: Record<string, string> = {
|
||||||
|
start: "talk.ptt.start",
|
||||||
|
stop: "talk.ptt.stop",
|
||||||
|
once: "talk.ptt.once",
|
||||||
|
cancel: "talk.ptt.cancel",
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeNodeKey(value: string) {
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+/, "")
|
||||||
|
.replace(/-+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isIOSNode(node: NodeSummary): boolean {
|
||||||
|
const platform = node.platform?.toLowerCase() ?? "";
|
||||||
|
const family = node.deviceFamily?.toLowerCase() ?? "";
|
||||||
|
return (
|
||||||
|
platform.startsWith("ios") ||
|
||||||
|
family.includes("iphone") ||
|
||||||
|
family.includes("ipad") ||
|
||||||
|
family.includes("ios")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadNodes(cfg: OpenClawConfig): Promise<NodeSummary[]> {
|
||||||
|
try {
|
||||||
|
const res = await callGateway<{ nodes?: NodeSummary[] }>({
|
||||||
|
method: "node.list",
|
||||||
|
params: {},
|
||||||
|
config: cfg,
|
||||||
|
});
|
||||||
|
return Array.isArray(res.nodes) ? res.nodes : [];
|
||||||
|
} catch {
|
||||||
|
const res = await callGateway<{ pending?: unknown[]; paired?: NodeSummary[] }>({
|
||||||
|
method: "node.pair.list",
|
||||||
|
params: {},
|
||||||
|
config: cfg,
|
||||||
|
});
|
||||||
|
return Array.isArray(res.paired) ? res.paired : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeNodes(nodes: NodeSummary[]) {
|
||||||
|
return nodes
|
||||||
|
.map((node) => node.displayName || node.remoteIp || node.nodeId)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveNodeId(nodes: NodeSummary[], query?: string): string {
|
||||||
|
const trimmed = String(query ?? "").trim();
|
||||||
|
if (trimmed) {
|
||||||
|
const qNorm = normalizeNodeKey(trimmed);
|
||||||
|
const matches = nodes.filter((node) => {
|
||||||
|
if (node.nodeId === trimmed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (typeof node.remoteIp === "string" && node.remoteIp === trimmed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const name = typeof node.displayName === "string" ? node.displayName : "";
|
||||||
|
if (name && normalizeNodeKey(name) === qNorm) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (trimmed.length >= 6 && node.nodeId.startsWith(trimmed)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matches.length === 1) {
|
||||||
|
return matches[0].nodeId;
|
||||||
|
}
|
||||||
|
const known = describeNodes(nodes);
|
||||||
|
if (matches.length === 0) {
|
||||||
|
throw new Error(`unknown node: ${trimmed}${known ? ` (known: ${known})` : ""}`);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`ambiguous node: ${trimmed} (matches: ${matches
|
||||||
|
.map((node) => node.displayName || node.remoteIp || node.nodeId)
|
||||||
|
.join(", ")})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const iosNodes = nodes.filter(isIOSNode);
|
||||||
|
const iosConnected = iosNodes.filter((node) => node.connected);
|
||||||
|
const iosCandidates = iosConnected.length > 0 ? iosConnected : iosNodes;
|
||||||
|
if (iosCandidates.length === 1) {
|
||||||
|
return iosCandidates[0].nodeId;
|
||||||
|
}
|
||||||
|
if (iosCandidates.length > 1) {
|
||||||
|
throw new Error(
|
||||||
|
`multiple iOS nodes found (${describeNodes(iosCandidates)}); specify node=<id>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const connected = nodes.filter((node) => node.connected);
|
||||||
|
const fallback = connected.length > 0 ? connected : nodes;
|
||||||
|
if (fallback.length === 1) {
|
||||||
|
return fallback[0].nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const known = describeNodes(nodes);
|
||||||
|
throw new Error(`node required${known ? ` (known: ${known})` : ""}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePTTArgs(commandBody: string) {
|
||||||
|
const tokens = commandBody.trim().split(/\s+/).slice(1);
|
||||||
|
let action: string | undefined;
|
||||||
|
let node: string | undefined;
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (!token) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (token.toLowerCase().startsWith("node=")) {
|
||||||
|
node = token.slice("node=".length);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!action) {
|
||||||
|
action = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { action, node };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPTTHelpText() {
|
||||||
|
return [
|
||||||
|
"Usage: /ptt <start|stop|once|cancel> [node=<id>]",
|
||||||
|
"Example: /ptt once node=iphone",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handlePTTCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||||
|
if (!allowTextCommands) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { command, cfg } = params;
|
||||||
|
const normalized = command.commandBodyNormalized.trim();
|
||||||
|
if (!normalized.startsWith("/ptt")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!command.isAuthorizedSender) {
|
||||||
|
logVerbose(`Ignoring /ptt from unauthorized sender: ${command.senderId || "<unknown>"}`);
|
||||||
|
return { shouldContinue: false, reply: { text: "PTT requires an authorized sender." } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parsePTTArgs(normalized);
|
||||||
|
const actionKey = parsed.action?.trim().toLowerCase() ?? "";
|
||||||
|
const commandId = PTT_COMMANDS[actionKey];
|
||||||
|
if (!commandId) {
|
||||||
|
return { shouldContinue: false, reply: { text: buildPTTHelpText() } };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const nodes = await loadNodes(cfg);
|
||||||
|
const nodeId = resolveNodeId(nodes, parsed.node);
|
||||||
|
const invokeParams: Record<string, unknown> = {
|
||||||
|
nodeId,
|
||||||
|
command: commandId,
|
||||||
|
params: {},
|
||||||
|
idempotencyKey: randomIdempotencyKey(),
|
||||||
|
timeoutMs: 15_000,
|
||||||
|
};
|
||||||
|
const res = await callGateway<{
|
||||||
|
ok?: boolean;
|
||||||
|
payload?: Record<string, unknown>;
|
||||||
|
command?: string;
|
||||||
|
nodeId?: string;
|
||||||
|
}>({
|
||||||
|
method: "node.invoke",
|
||||||
|
params: invokeParams,
|
||||||
|
config: cfg,
|
||||||
|
});
|
||||||
|
const payload =
|
||||||
|
res.payload && typeof res.payload === "object"
|
||||||
|
? (res.payload as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const lines = [`PTT ${actionKey} → ${nodeId}`];
|
||||||
|
if (typeof payload.status === "string") {
|
||||||
|
lines.push(`status: ${payload.status}`);
|
||||||
|
}
|
||||||
|
if (typeof payload.captureId === "string") {
|
||||||
|
lines.push(`captureId: ${payload.captureId}`);
|
||||||
|
}
|
||||||
|
if (typeof payload.transcript === "string" && payload.transcript.trim()) {
|
||||||
|
lines.push(`transcript: ${payload.transcript}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { shouldContinue: false, reply: { text: lines.join("\n") } };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
return { shouldContinue: false, reply: { text: `PTT failed: ${message}` } };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -251,4 +251,23 @@ describe("nodes-cli coverage", () => {
|
||||||
});
|
});
|
||||||
expect(invoke?.params?.timeoutMs).toBe(6000);
|
expect(invoke?.params?.timeoutMs).toBe(6000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("invokes talk.ptt.once via nodes talk ptt once", async () => {
|
||||||
|
runtimeLogs.length = 0;
|
||||||
|
runtimeErrors.length = 0;
|
||||||
|
callGateway.mockClear();
|
||||||
|
randomIdempotencyKey.mockClear();
|
||||||
|
|
||||||
|
const { registerNodesCli } = await import("./nodes-cli.js");
|
||||||
|
const program = new Command();
|
||||||
|
program.exitOverride();
|
||||||
|
registerNodesCli(program);
|
||||||
|
|
||||||
|
await program.parseAsync(["nodes", "talk", "ptt", "once", "--node", "mac-1"], { from: "user" });
|
||||||
|
|
||||||
|
const invoke = callGateway.mock.calls.find((call) => call[0]?.method === "node.invoke")?.[0];
|
||||||
|
expect(invoke).toBeTruthy();
|
||||||
|
expect(invoke?.params?.command).toBe("talk.ptt.once");
|
||||||
|
expect(invoke?.params?.idempotencyKey).toBe("rk_test");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
import type { Command } from "commander";
|
||||||
|
import { randomIdempotencyKey } from "../../gateway/call.js";
|
||||||
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
|
import { runNodesCommand } from "./cli-utils.js";
|
||||||
|
import { callGatewayCli, nodesCallOpts, resolveNodeId } from "./rpc.js";
|
||||||
|
import type { NodesRpcOpts } from "./types.js";
|
||||||
|
|
||||||
|
type PTTAction = {
|
||||||
|
name: string;
|
||||||
|
command: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PTT_ACTIONS: PTTAction[] = [
|
||||||
|
{ name: "start", command: "talk.ptt.start", description: "Start push-to-talk capture" },
|
||||||
|
{ name: "stop", command: "talk.ptt.stop", description: "Stop push-to-talk capture" },
|
||||||
|
{ name: "once", command: "talk.ptt.once", description: "Run push-to-talk once" },
|
||||||
|
{ name: "cancel", command: "talk.ptt.cancel", description: "Cancel push-to-talk capture" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function registerNodesTalkCommands(nodes: Command) {
|
||||||
|
const talk = nodes.command("talk").description("Talk/voice controls on a paired node");
|
||||||
|
const ptt = talk.command("ptt").description("Push-to-talk controls");
|
||||||
|
|
||||||
|
for (const action of PTT_ACTIONS) {
|
||||||
|
nodesCallOpts(
|
||||||
|
ptt
|
||||||
|
.command(action.name)
|
||||||
|
.description(action.description)
|
||||||
|
.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP")
|
||||||
|
.option("--invoke-timeout <ms>", "Node invoke timeout in ms (default 15000)", "15000")
|
||||||
|
.action(async (opts: NodesRpcOpts) => {
|
||||||
|
await runNodesCommand(`talk ptt ${action.name}`, async () => {
|
||||||
|
const nodeId = await resolveNodeId(opts, String(opts.node ?? ""));
|
||||||
|
const invokeTimeoutMs = opts.invokeTimeout
|
||||||
|
? Number.parseInt(String(opts.invokeTimeout), 10)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const invokeParams: Record<string, unknown> = {
|
||||||
|
nodeId,
|
||||||
|
command: action.command,
|
||||||
|
params: {},
|
||||||
|
idempotencyKey: randomIdempotencyKey(),
|
||||||
|
};
|
||||||
|
if (typeof invokeTimeoutMs === "number" && Number.isFinite(invokeTimeoutMs)) {
|
||||||
|
invokeParams.timeoutMs = invokeTimeoutMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await callGatewayCli("node.invoke", opts, invokeParams);
|
||||||
|
const res =
|
||||||
|
typeof raw === "object" && raw !== null ? (raw as { payload?: unknown }) : {};
|
||||||
|
const payload =
|
||||||
|
res.payload && typeof res.payload === "object"
|
||||||
|
? (res.payload as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (opts.json) {
|
||||||
|
defaultRuntime.log(JSON.stringify(payload, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = [`PTT ${action.name} → ${nodeId}`];
|
||||||
|
if (typeof payload.status === "string") {
|
||||||
|
lines.push(`status: ${payload.status}`);
|
||||||
|
}
|
||||||
|
if (typeof payload.captureId === "string") {
|
||||||
|
lines.push(`captureId: ${payload.captureId}`);
|
||||||
|
}
|
||||||
|
if (typeof payload.transcript === "string" && payload.transcript.trim()) {
|
||||||
|
lines.push(`transcript: ${payload.transcript}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRuntime.log(lines.join("\n"));
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
{ timeoutMs: 30_000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import { registerNodesNotifyCommand } from "./register.notify.js";
|
||||||
import { registerNodesPairingCommands } from "./register.pairing.js";
|
import { registerNodesPairingCommands } from "./register.pairing.js";
|
||||||
import { registerNodesScreenCommands } from "./register.screen.js";
|
import { registerNodesScreenCommands } from "./register.screen.js";
|
||||||
import { registerNodesStatusCommands } from "./register.status.js";
|
import { registerNodesStatusCommands } from "./register.status.js";
|
||||||
|
import { registerNodesTalkCommands } from "./register.talk.js";
|
||||||
|
|
||||||
export function registerNodesCli(program: Command) {
|
export function registerNodesCli(program: Command) {
|
||||||
const nodes = program
|
const nodes = program
|
||||||
|
|
@ -28,4 +29,5 @@ export function registerNodesCli(program: Command) {
|
||||||
registerNodesCameraCommands(nodes);
|
registerNodesCameraCommands(nodes);
|
||||||
registerNodesScreenCommands(nodes);
|
registerNodesScreenCommands(nodes);
|
||||||
registerNodesLocationCommands(nodes);
|
registerNodesLocationCommands(nodes);
|
||||||
|
registerNodesTalkCommands(nodes);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue