@linkedclaw/cli 0.1.2 → 0.1.3
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 +90 -52
- package/dist/bin.js +6261 -4698
- package/dist/bin.js.map +1 -1
- package/package.json +17 -32
- package/src/bin.ts +23 -0
- package/src/commands/auth.ts +116 -0
- package/src/commands/provider.ts +245 -0
- package/src/commands/requester.ts +436 -0
- package/src/config.ts +76 -0
- package/src/context.ts +27 -0
- package/src/errors.ts +41 -0
- package/src/handlers/subprocess.ts +185 -0
- package/src/output.ts +57 -0
- package/src/types.ts +90 -0
- package/test/cli-help.test.ts +62 -0
- package/test/hire-flags.test.ts +55 -0
- package/test/recv-flags.test.ts +83 -0
- package/test/register-browser.test.ts +55 -0
- package/tsconfig.json +14 -0
- package/tsup.config.ts +25 -0
- package/vitest.config.ts +8 -0
package/package.json
CHANGED
|
@@ -1,49 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkedclaw/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "Apache-2.0",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "LinkedClaw command-line interface",
|
|
6
5
|
"type": "module",
|
|
7
6
|
"bin": {
|
|
8
7
|
"linkedclaw": "./dist/bin.js"
|
|
9
8
|
},
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"LICENSE",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"keywords": [
|
|
16
|
-
"linkedclaw",
|
|
17
|
-
"cli",
|
|
18
|
-
"agent",
|
|
19
|
-
"marketplace"
|
|
20
|
-
],
|
|
21
|
-
"repository": {
|
|
22
|
-
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/linkedclaw/linkedclaw.git",
|
|
24
|
-
"directory": "typescript/packages/cli"
|
|
25
|
-
},
|
|
26
|
-
"publishConfig": {
|
|
27
|
-
"access": "public"
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=20"
|
|
28
11
|
},
|
|
29
12
|
"dependencies": {
|
|
30
|
-
"@linkedclaw/core": "^0.1.2",
|
|
31
13
|
"commander": "^12.1.0",
|
|
32
|
-
"js-yaml": "^4.1.0"
|
|
14
|
+
"js-yaml": "^4.1.0",
|
|
15
|
+
"open": "^10.1.0",
|
|
16
|
+
"ws": "^8.0.0",
|
|
17
|
+
"@linkedclaw/provider-runtime": "^0.9.1",
|
|
18
|
+
"@linkedclaw/consumer-runtime": "^0.9.1",
|
|
19
|
+
"@linkedclaw/provider": "^0.9.1",
|
|
20
|
+
"@linkedclaw/consumer": "^0.9.1"
|
|
33
21
|
},
|
|
34
22
|
"devDependencies": {
|
|
35
|
-
"@types/js-yaml": "^4.0.9"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
23
|
+
"@types/js-yaml": "^4.0.9",
|
|
24
|
+
"@types/node": "^20.0.0",
|
|
25
|
+
"tsup": "^8.0.0",
|
|
26
|
+
"typescript": "^5.4.0",
|
|
27
|
+
"vitest": "^1.6.0"
|
|
39
28
|
},
|
|
40
29
|
"scripts": {
|
|
41
30
|
"build": "tsup",
|
|
42
|
-
"dev": "tsup --watch",
|
|
43
|
-
"test": "vitest run",
|
|
44
|
-
"test:watch": "vitest",
|
|
45
31
|
"typecheck": "tsc --noEmit",
|
|
46
|
-
"
|
|
47
|
-
"start": "node dist/bin.js"
|
|
32
|
+
"test": "vitest run"
|
|
48
33
|
}
|
|
49
34
|
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { registerAuthCommands } from "./commands/auth.js";
|
|
3
|
+
import { registerProviderCommands } from "./commands/provider.js";
|
|
4
|
+
import { registerRequesterCommands } from "./commands/requester.js";
|
|
5
|
+
|
|
6
|
+
const CLI_VERSION = "0.1.2";
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name("linkedclaw")
|
|
11
|
+
.description("Official LinkedClaw CLI — any agent can shell out to hire providers, invoke, or broadcast")
|
|
12
|
+
.version(`cli ${CLI_VERSION}`);
|
|
13
|
+
|
|
14
|
+
registerAuthCommands(program);
|
|
15
|
+
registerRequesterCommands(program);
|
|
16
|
+
registerProviderCommands(program);
|
|
17
|
+
|
|
18
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
19
|
+
process.stderr.write(
|
|
20
|
+
JSON.stringify({ error: "internal_error", message: err instanceof Error ? err.message : String(err) }) + "\n",
|
|
21
|
+
);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { configPath, readFileConfig, writeFileConfig, DEFAULT_CLOUD_URL } from "../config.js";
|
|
3
|
+
import { buildContext } from "../context.js";
|
|
4
|
+
import { runCommand, readLine } from "../output.js";
|
|
5
|
+
|
|
6
|
+
export function registerAuthCommands(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("login")
|
|
9
|
+
.description("Store API key in ~/.linkedclaw/config.yaml")
|
|
10
|
+
.option("--api-key <key>", "API key (otherwise read from stdin)")
|
|
11
|
+
.option("--cloud-url <url>", "Override cloud URL")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
await runCommand(async () => {
|
|
14
|
+
let apiKey = opts.apiKey as string | undefined;
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
apiKey = await readLine("Paste API key: ");
|
|
17
|
+
}
|
|
18
|
+
if (!apiKey) throw new Error("empty api key");
|
|
19
|
+
const prev = readFileConfig();
|
|
20
|
+
const next = { ...prev, apiKey, ...(opts.cloudUrl ? { cloudUrl: opts.cloudUrl } : {}) };
|
|
21
|
+
writeFileConfig(next);
|
|
22
|
+
return { ok: true, path: configPath() };
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command("register")
|
|
28
|
+
.description("Open browser to create a LinkedClaw account, then paste your API key")
|
|
29
|
+
.option("--no-browser", "Print URL instead of attempting to open the browser")
|
|
30
|
+
.option("--cloud-url <url>", "Override cloud URL")
|
|
31
|
+
.action(async (opts) => {
|
|
32
|
+
await runCommand(async () => {
|
|
33
|
+
const prev = readFileConfig();
|
|
34
|
+
const cloudUrl =
|
|
35
|
+
(opts.cloudUrl as string | undefined) ??
|
|
36
|
+
(prev.cloudUrl as string | undefined) ??
|
|
37
|
+
process.env.LINKEDCLAW_CLOUD_URL ??
|
|
38
|
+
DEFAULT_CLOUD_URL;
|
|
39
|
+
const portalUrl = cloudUrl.replace(/\/$/, "") + "/register";
|
|
40
|
+
|
|
41
|
+
let opened = false;
|
|
42
|
+
if (opts.browser !== false) {
|
|
43
|
+
try {
|
|
44
|
+
const open = (await import("open")).default;
|
|
45
|
+
await open(portalUrl);
|
|
46
|
+
opened = true;
|
|
47
|
+
} catch {
|
|
48
|
+
// headless / no DISPLAY / package missing → fall through to URL print
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!opened) {
|
|
52
|
+
process.stderr.write(`Open this URL in a browser to register:\n ${portalUrl}\n\n`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const apiKey = await readLine("Paste your API key (from portal Settings → API Keys): ");
|
|
56
|
+
if (!apiKey) throw new Error("empty api key");
|
|
57
|
+
|
|
58
|
+
const next = {
|
|
59
|
+
...prev,
|
|
60
|
+
apiKey,
|
|
61
|
+
cloudUrl,
|
|
62
|
+
};
|
|
63
|
+
writeFileConfig(next);
|
|
64
|
+
return { ok: true, path: configPath(), opened };
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command("whoami")
|
|
70
|
+
.description("Print current user info")
|
|
71
|
+
.option("--human", "Human-readable output")
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
await runCommand(async () => {
|
|
74
|
+
const { consumer } = buildContext();
|
|
75
|
+
return consumer.getMe();
|
|
76
|
+
}, { human: opts.human });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const config = program
|
|
80
|
+
.command("config")
|
|
81
|
+
.description("Inspect or edit local config file");
|
|
82
|
+
|
|
83
|
+
config
|
|
84
|
+
.command("show")
|
|
85
|
+
.description("Print the config file contents (api key redacted)")
|
|
86
|
+
.action(async () => {
|
|
87
|
+
await runCommand(async () => {
|
|
88
|
+
const raw = readFileConfig();
|
|
89
|
+
const redacted = { ...raw };
|
|
90
|
+
if (redacted.apiKey) redacted.apiKey = "lc_***" + String(redacted.apiKey).slice(-4);
|
|
91
|
+
return { path: configPath(), config: redacted };
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
config
|
|
96
|
+
.command("set <key> <value>")
|
|
97
|
+
.description("Set a config key (simple top-level keys only)")
|
|
98
|
+
.action(async (key: string, value: string) => {
|
|
99
|
+
await runCommand(async () => {
|
|
100
|
+
const prev = readFileConfig();
|
|
101
|
+
const next: Record<string, unknown> = { ...prev };
|
|
102
|
+
const parsed = tryParseJson(value);
|
|
103
|
+
next[key] = parsed;
|
|
104
|
+
writeFileConfig(next);
|
|
105
|
+
return { ok: true, key, value: parsed };
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function tryParseJson(v: string): unknown {
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(v);
|
|
113
|
+
} catch {
|
|
114
|
+
return v;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { load as yamlLoad } from "js-yaml";
|
|
4
|
+
import {
|
|
5
|
+
ProviderRuntime,
|
|
6
|
+
RelayClient,
|
|
7
|
+
type ProviderHandler,
|
|
8
|
+
} from "@linkedclaw/provider-runtime";
|
|
9
|
+
import type { CreateAgentRequest, UpdateAgentRequest, BroadcastSubmitRequest } from "../types.js";
|
|
10
|
+
import type { ProviderConfig } from "../config.js";
|
|
11
|
+
import { buildContext } from "../context.js";
|
|
12
|
+
import { SubprocessHandler } from "../handlers/subprocess.js";
|
|
13
|
+
import { readStdin, runCommand, printError } from "../output.js";
|
|
14
|
+
|
|
15
|
+
export function registerProviderCommands(program: Command): void {
|
|
16
|
+
const provider = program.command("provider").description("Provider-side commands");
|
|
17
|
+
|
|
18
|
+
provider
|
|
19
|
+
.command("register <config>")
|
|
20
|
+
.description("Register (create/update) an agent listing from a provider YAML file. Use \"-\" for stdin.")
|
|
21
|
+
.option("--human", "Human-readable output")
|
|
22
|
+
.action(async (path: string, opts) => {
|
|
23
|
+
await runCommand(async () => {
|
|
24
|
+
const cfg = await loadProviderYaml(path);
|
|
25
|
+
const { providerClient } = buildContext();
|
|
26
|
+
const body = buildCreateAgentRequest(cfg);
|
|
27
|
+
if (cfg.agentId) {
|
|
28
|
+
return providerClient.updateAgent(cfg.agentId, body as unknown as Record<string, unknown>);
|
|
29
|
+
}
|
|
30
|
+
return providerClient.createAgent({
|
|
31
|
+
slug: body.slug,
|
|
32
|
+
name: body.name,
|
|
33
|
+
description: body.description ?? "",
|
|
34
|
+
capabilities: body.capabilities,
|
|
35
|
+
});
|
|
36
|
+
}, { human: opts.human });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
provider
|
|
40
|
+
.command("update <listing_id>")
|
|
41
|
+
.description("Patch an existing agent listing. Body = JSON file or stdin.")
|
|
42
|
+
.requiredOption("--body <json>", 'JSON body (or "-" to read from stdin)')
|
|
43
|
+
.option("--human", "Human-readable output")
|
|
44
|
+
.action(async (listingId: string, opts) => {
|
|
45
|
+
await runCommand(async () => {
|
|
46
|
+
const raw = opts.body === "-" ? await readStdin() : readFileSync(opts.body, "utf8");
|
|
47
|
+
const body = JSON.parse(raw) as UpdateAgentRequest;
|
|
48
|
+
const { providerClient } = buildContext();
|
|
49
|
+
return providerClient.updateAgent(listingId, body as unknown as Record<string, unknown>);
|
|
50
|
+
}, { human: opts.human });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
provider
|
|
54
|
+
.command("listings")
|
|
55
|
+
.description("Show my own agent listings (as provider)")
|
|
56
|
+
.option("--human", "Human-readable output")
|
|
57
|
+
.action(async (opts) => {
|
|
58
|
+
await runCommand(async () => {
|
|
59
|
+
const { consumer } = buildContext();
|
|
60
|
+
return consumer.discover({ owner: "me" });
|
|
61
|
+
}, { human: opts.human });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
provider
|
|
65
|
+
.command("run <config>")
|
|
66
|
+
.description("Run a provider daemon: connect WS, dispatch events to handler subprocess")
|
|
67
|
+
.option("--handler-cmd <cmd>", "Shell command to spawn as the handler child process")
|
|
68
|
+
.option("--handler-http <url>", "HTTP webhook URL to POST each event to (alternative to --handler-cmd)")
|
|
69
|
+
.option("--human", "Human-readable status output")
|
|
70
|
+
.action(async (path: string, opts) => {
|
|
71
|
+
try {
|
|
72
|
+
const yamlCfg = await loadProviderYaml(path);
|
|
73
|
+
if (!yamlCfg.agentId) {
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
JSON.stringify({ error: "provider_unconfigured", message: "agentId missing — run `linkedclaw provider register` first or set it in YAML" }) + "\n",
|
|
76
|
+
);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
if (!opts.handlerCmd && !opts.handlerHttp) {
|
|
80
|
+
process.stderr.write(
|
|
81
|
+
JSON.stringify({ error: "config_error", message: "either --handler-cmd or --handler-http is required" }) + "\n",
|
|
82
|
+
);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
if (opts.handlerCmd && opts.handlerHttp) {
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
JSON.stringify({ error: "config_error", message: "--handler-cmd and --handler-http are mutually exclusive" }) + "\n",
|
|
88
|
+
);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { providerClient, cfg: merged } = buildContext({ ...yamlCfg });
|
|
93
|
+
const relay = new RelayClient({
|
|
94
|
+
url: merged.relayUrl,
|
|
95
|
+
apiKey: merged.apiKey!,
|
|
96
|
+
agentId: merged.agentId!,
|
|
97
|
+
});
|
|
98
|
+
const handler = opts.handlerCmd
|
|
99
|
+
? new SubprocessHandler({ cmd: opts.handlerCmd as string })
|
|
100
|
+
: makeHttpHandler(opts.handlerHttp as string);
|
|
101
|
+
|
|
102
|
+
const runtime = new ProviderRuntime({
|
|
103
|
+
cloud: {
|
|
104
|
+
broadcasts: {
|
|
105
|
+
accept: (taskId, body) => providerClient.acceptGigTask(taskId, body as unknown as Record<string, unknown>),
|
|
106
|
+
submit: (taskId, body) => providerClient.submitGigTask(taskId, body as unknown as Record<string, unknown>),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
relay,
|
|
110
|
+
handler,
|
|
111
|
+
config: merged,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
let stopping = false;
|
|
115
|
+
const shutdown = async (sig: string) => {
|
|
116
|
+
if (stopping) return;
|
|
117
|
+
stopping = true;
|
|
118
|
+
process.stderr.write(`\n[provider] received ${sig}, stopping...\n`);
|
|
119
|
+
await runtime.stop();
|
|
120
|
+
if (handler instanceof SubprocessHandler) await handler.close();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
};
|
|
123
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
124
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
125
|
+
|
|
126
|
+
relay.on("connected", () => {
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
JSON.stringify({ event: "connected", agent_id: merged.agentId }) + "\n",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
relay.on("disconnected", (reason) => {
|
|
132
|
+
process.stderr.write(JSON.stringify({ event: "disconnected", reason }) + "\n");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
await runtime.run();
|
|
136
|
+
process.stderr.write(JSON.stringify({ event: "provider_running", agent_id: merged.agentId }) + "\n");
|
|
137
|
+
// Keep the process alive; runtime.run() returns after initial connect.
|
|
138
|
+
await new Promise<void>(() => {});
|
|
139
|
+
} catch (err) {
|
|
140
|
+
printError(err);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
provider
|
|
146
|
+
.command("pick <bct_id>")
|
|
147
|
+
.description("Manually accept a broadcast task (provider side)")
|
|
148
|
+
.requiredOption("--agent-id <agt_id>", "Which of your agents is accepting")
|
|
149
|
+
.option("--slot-key <key>", "Slot key for sliced broadcasts")
|
|
150
|
+
.option("--human", "Human-readable output")
|
|
151
|
+
.action(async (taskId: string, opts) => {
|
|
152
|
+
await runCommand(async () => {
|
|
153
|
+
const { providerClient } = buildContext();
|
|
154
|
+
const body: { agent_id: string; slot_key?: string } = { agent_id: opts.agentId };
|
|
155
|
+
if (opts.slotKey !== undefined) body.slot_key = opts.slotKey;
|
|
156
|
+
return providerClient.acceptGigTask(taskId, body as unknown as Record<string, unknown>);
|
|
157
|
+
}, { human: opts.human });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
provider
|
|
161
|
+
.command("submit <bct_id> <result_file>")
|
|
162
|
+
.description('Submit a broadcast result. result_file = JSON path or "-" for stdin.')
|
|
163
|
+
.option("--human", "Human-readable output")
|
|
164
|
+
.action(async (taskId: string, resultFile: string, opts) => {
|
|
165
|
+
await runCommand(async () => {
|
|
166
|
+
const raw = resultFile === "-" ? await readStdin() : readFileSync(resultFile, "utf8");
|
|
167
|
+
const body = JSON.parse(raw) as BroadcastSubmitRequest;
|
|
168
|
+
const { providerClient } = buildContext();
|
|
169
|
+
return providerClient.submitGigTask(taskId, body as unknown as Record<string, unknown>);
|
|
170
|
+
}, { human: opts.human });
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ───── helpers ─────
|
|
175
|
+
|
|
176
|
+
async function loadProviderYaml(path: string): Promise<Partial<ProviderConfig>> {
|
|
177
|
+
const raw = path === "-" ? await readStdin() : readFileSync(path, "utf8");
|
|
178
|
+
const parsed = yamlLoad(raw);
|
|
179
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
180
|
+
throw new Error(`provider config ${path} is not a YAML object`);
|
|
181
|
+
}
|
|
182
|
+
return parsed as Partial<ProviderConfig>;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildCreateAgentRequest(
|
|
186
|
+
cfg: Partial<ProviderConfig>,
|
|
187
|
+
): CreateAgentRequest {
|
|
188
|
+
if (!cfg.slug) throw new Error("provider config missing `slug`");
|
|
189
|
+
if (!cfg.agentName) throw new Error("provider config missing `agentName`");
|
|
190
|
+
if (!cfg.capabilities || cfg.capabilities.length === 0) {
|
|
191
|
+
throw new Error("provider config missing `capabilities`");
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
slug: cfg.slug,
|
|
195
|
+
name: cfg.agentName,
|
|
196
|
+
description: cfg.description ?? "",
|
|
197
|
+
capabilities: cfg.capabilities,
|
|
198
|
+
...(cfg.pricingModel !== undefined ? { pricing_model: cfg.pricingModel as CreateAgentRequest["pricing_model"] } : {}),
|
|
199
|
+
...(cfg.priceCredits !== undefined ? { price_credits: cfg.priceCredits } : {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function makeHttpHandler(url: string): ProviderHandler {
|
|
204
|
+
return {
|
|
205
|
+
async onSessionCreate(evt) {
|
|
206
|
+
return (await postEvent(url, evt)) as { accept: boolean; reason?: string };
|
|
207
|
+
},
|
|
208
|
+
async onSessionMessage(evt) {
|
|
209
|
+
const r = await postEvent(url, evt);
|
|
210
|
+
if (r === null || r === undefined) return null;
|
|
211
|
+
if (typeof r === "string") return r;
|
|
212
|
+
if (typeof r === "object") {
|
|
213
|
+
const obj = r as Record<string, unknown>;
|
|
214
|
+
if (typeof obj["content"] === "string") return obj["content"] as string;
|
|
215
|
+
if ("payload" in obj) return obj as { payload: unknown };
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
},
|
|
219
|
+
async onSessionEnd(evt) {
|
|
220
|
+
await postEvent(url, evt);
|
|
221
|
+
},
|
|
222
|
+
async onInvoke(evt) {
|
|
223
|
+
return (await postEvent(url, evt)) as {
|
|
224
|
+
output?: Record<string, unknown>;
|
|
225
|
+
error?: { code: string; message: string };
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
async onBroadcastOffer(evt) {
|
|
229
|
+
return (await postEvent(url, evt)) as { accept: boolean; slot_key?: string };
|
|
230
|
+
},
|
|
231
|
+
async onBroadcastExecute(evt) {
|
|
232
|
+
return (await postEvent(url, evt)) as { result_payload?: Record<string, unknown> };
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function postEvent(url: string, body: object): Promise<unknown> {
|
|
238
|
+
const res = await fetch(url, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
headers: { "Content-Type": "application/json" },
|
|
241
|
+
body: JSON.stringify(body),
|
|
242
|
+
});
|
|
243
|
+
if (!res.ok) throw new Error(`handler HTTP ${res.status}`);
|
|
244
|
+
return res.json();
|
|
245
|
+
}
|