@shadowob/cli 1.1.3 → 1.1.5
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/dist/index.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
configManager,
|
|
4
4
|
getClient,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
getClientWithToken,
|
|
6
|
+
getSocket,
|
|
7
|
+
parsePositiveInt
|
|
8
|
+
} from "./chunk-E364BDQO.js";
|
|
7
9
|
|
|
8
10
|
// src/index.ts
|
|
9
|
-
import { Command as
|
|
11
|
+
import { Command as Command28 } from "commander";
|
|
10
12
|
|
|
11
13
|
// src/commands/agents.ts
|
|
12
14
|
import { Command } from "commander";
|
|
@@ -265,11 +267,169 @@ function createApiTokensCommand() {
|
|
|
265
267
|
return tokens;
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
// src/commands/app.ts
|
|
271
|
+
import { readFile, writeFile } from "fs/promises";
|
|
272
|
+
import { basename } from "path";
|
|
273
|
+
import { Command as Command3 } from "commander";
|
|
274
|
+
function resolveServer(value) {
|
|
275
|
+
const server = value ?? process.env.SHADOWOB_SERVER_ID;
|
|
276
|
+
if (!server) throw new Error("Missing server. Pass --server or set SHADOWOB_SERVER_ID.");
|
|
277
|
+
return server;
|
|
278
|
+
}
|
|
279
|
+
function parseJsonInput(value) {
|
|
280
|
+
if (!value) return {};
|
|
281
|
+
const parsed = JSON.parse(value);
|
|
282
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "input" in parsed && Object.keys(parsed).every((key) => key === "input" || key === "channelId")) {
|
|
283
|
+
return parsed.input ?? {};
|
|
284
|
+
}
|
|
285
|
+
return parsed;
|
|
286
|
+
}
|
|
287
|
+
async function readJsonFile(path) {
|
|
288
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
289
|
+
}
|
|
290
|
+
function commandHandlerError(error, json) {
|
|
291
|
+
outputError(error instanceof Error ? error.message : String(error), { json });
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
function createAppCommand() {
|
|
295
|
+
const app = new Command3("app").description("Server App integration commands");
|
|
296
|
+
app.command("list").description("List apps installed in a server").requiredOption("--server <server>", "Server ID or slug").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
297
|
+
try {
|
|
298
|
+
const client = await getClient(options.profile);
|
|
299
|
+
output(await client.listServerApps(resolveServer(options.server)), { json: options.json });
|
|
300
|
+
} catch (error) {
|
|
301
|
+
commandHandlerError(error, options.json);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
app.command("preview").description("Discover and preview a server App manifest before installing it").requiredOption("--server <server>", "Server ID or slug").option("--manifest-url <url>", "Manifest URL").option("--manifest-file <path>", "Local manifest JSON file").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
305
|
+
async (options) => {
|
|
306
|
+
try {
|
|
307
|
+
if (!options.manifestUrl && !options.manifestFile) {
|
|
308
|
+
throw new Error("Pass --manifest-url or --manifest-file");
|
|
309
|
+
}
|
|
310
|
+
const client = await getClient(options.profile);
|
|
311
|
+
const manifest = options.manifestFile ? await readJsonFile(options.manifestFile) : void 0;
|
|
312
|
+
output(
|
|
313
|
+
await client.discoverServerApp(resolveServer(options.server), {
|
|
314
|
+
manifestUrl: options.manifestUrl,
|
|
315
|
+
manifest
|
|
316
|
+
}),
|
|
317
|
+
{ json: options.json }
|
|
318
|
+
);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
commandHandlerError(error, options.json);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
app.command("install").description("Install or update a server App from a manifest").requiredOption("--server <server>", "Server ID or slug").option("--manifest-url <url>", "Manifest URL").option("--manifest-file <path>", "Local manifest JSON file").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
325
|
+
async (options) => {
|
|
326
|
+
try {
|
|
327
|
+
if (!options.manifestUrl && !options.manifestFile) {
|
|
328
|
+
throw new Error("Pass --manifest-url or --manifest-file");
|
|
329
|
+
}
|
|
330
|
+
const client = await getClient(options.profile);
|
|
331
|
+
const manifest = options.manifestFile ? await readJsonFile(options.manifestFile) : void 0;
|
|
332
|
+
const result = await client.installServerApp(resolveServer(options.server), {
|
|
333
|
+
manifestUrl: options.manifestUrl,
|
|
334
|
+
manifest
|
|
335
|
+
});
|
|
336
|
+
output(result, { json: options.json });
|
|
337
|
+
} catch (error) {
|
|
338
|
+
commandHandlerError(error, options.json);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
);
|
|
342
|
+
app.command("inspect").description("Inspect an installed server App").argument("<app-key>", "App key").requiredOption("--server <server>", "Server ID or slug").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
343
|
+
async (appKey, options) => {
|
|
344
|
+
try {
|
|
345
|
+
const client = await getClient(options.profile);
|
|
346
|
+
output(await client.getServerApp(resolveServer(options.server), appKey), {
|
|
347
|
+
json: options.json
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
commandHandlerError(error, options.json);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
app.command("grant").description("Grant a Buddy access to an installed server App").argument("<app-key>", "App key").requiredOption("--server <server>", "Server ID or slug").requiredOption("--buddy <agent-id>", "Buddy agent ID").requiredOption("--permissions <permissions>", "Comma-separated permissions, or *").option("--approval-mode <mode>", "none, first_time, every_time, or policy", "none").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
355
|
+
async (appKey, options) => {
|
|
356
|
+
try {
|
|
357
|
+
const client = await getClient(options.profile);
|
|
358
|
+
const permissions = options.permissions.split(",").map((item) => item.trim()).filter(Boolean);
|
|
359
|
+
const result = await client.grantServerAppToBuddy(resolveServer(options.server), appKey, {
|
|
360
|
+
buddyAgentId: options.buddy,
|
|
361
|
+
permissions,
|
|
362
|
+
approvalMode: options.approvalMode
|
|
363
|
+
});
|
|
364
|
+
output(result, { json: options.json });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
commandHandlerError(error, options.json);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
);
|
|
370
|
+
app.command("discover").description("Emit Skill-style command discovery for server Apps").requiredOption("--server <server>", "Server ID or slug").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
371
|
+
try {
|
|
372
|
+
const client = await getClient(options.profile);
|
|
373
|
+
const server = resolveServer(options.server);
|
|
374
|
+
const apps = await client.listServerApps(server);
|
|
375
|
+
const docs = await Promise.all(
|
|
376
|
+
apps.map((entry) => client.getServerAppSkills(server, entry.appKey))
|
|
377
|
+
);
|
|
378
|
+
if (options.json) {
|
|
379
|
+
output(docs, { json: true });
|
|
380
|
+
} else {
|
|
381
|
+
console.log(docs.map((doc) => doc.markdown).join("\n\n---\n\n"));
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
commandHandlerError(error, options.json);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
app.command("skills").description("Emit Skill text for one installed server App").argument("<app-key>", "App key").requiredOption("--server <server>", "Server ID or slug").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
388
|
+
async (appKey, options) => {
|
|
389
|
+
try {
|
|
390
|
+
const client = await getClient(options.profile);
|
|
391
|
+
const result = await client.getServerAppSkills(resolveServer(options.server), appKey);
|
|
392
|
+
if (options.json) output(result, { json: true });
|
|
393
|
+
else console.log(result.markdown);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
commandHandlerError(error, options.json);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
app.command("call").description("Call a server App command").argument("<app-key>", "App key").argument("<command>", "Command name").requiredOption("--server <server>", "Server ID or slug").option("--json-input <json>", "JSON command input").option("--input-file <path>", "Read JSON command input from file").option("--file <path>", "Attach a binary file").option("--field <field>", "Multipart file field name", "file").option("--output <path>", "Write binary dataBase64 response to this path").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
400
|
+
async (appKey, commandName, options) => {
|
|
401
|
+
try {
|
|
402
|
+
const client = await getClient(options.profile);
|
|
403
|
+
const input = options.inputFile ? await readJsonFile(options.inputFile) : parseJsonInput(options.jsonInput);
|
|
404
|
+
const server = resolveServer(options.server);
|
|
405
|
+
const result = options.file ? await client.callServerAppCommandMultipart(server, appKey, commandName, {
|
|
406
|
+
input,
|
|
407
|
+
file: new Blob([await readFile(options.file)]),
|
|
408
|
+
filename: basename(options.file),
|
|
409
|
+
field: options.field
|
|
410
|
+
}) : await client.callServerAppCommand(server, appKey, commandName, { input });
|
|
411
|
+
if (options.output && result && typeof result === "object" && "dataBase64" in result && typeof result.dataBase64 === "string") {
|
|
412
|
+
await writeFile(
|
|
413
|
+
options.output,
|
|
414
|
+
Buffer.from(result.dataBase64, "base64")
|
|
415
|
+
);
|
|
416
|
+
outputSuccess(`Wrote ${options.output}`, { json: options.json });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
output(result, { json: options.json });
|
|
420
|
+
} catch (error) {
|
|
421
|
+
commandHandlerError(error, options.json);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
return app;
|
|
426
|
+
}
|
|
427
|
+
|
|
268
428
|
// src/commands/auth.ts
|
|
269
429
|
import { ShadowClient } from "@shadowob/sdk";
|
|
270
|
-
import { Command as
|
|
430
|
+
import { Command as Command4 } from "commander";
|
|
271
431
|
function createAuthCommand() {
|
|
272
|
-
const auth = new
|
|
432
|
+
const auth = new Command4("auth").description("Authentication commands");
|
|
273
433
|
auth.command("login").description("Authenticate with a Shadow server").requiredOption("--server-url <url>", "Shadow server URL").requiredOption("--token <token>", "JWT token").option("--profile <name>", "Profile name", "default").option("--json", "Output as JSON").action(
|
|
274
434
|
async (options) => {
|
|
275
435
|
try {
|
|
@@ -382,9 +542,9 @@ function createAuthCommand() {
|
|
|
382
542
|
}
|
|
383
543
|
|
|
384
544
|
// src/commands/channels.ts
|
|
385
|
-
import { Command as
|
|
545
|
+
import { Command as Command5 } from "commander";
|
|
386
546
|
function createChannelsCommand() {
|
|
387
|
-
const channels = new
|
|
547
|
+
const channels = new Command5("channels").description("Channel commands");
|
|
388
548
|
channels.command("list").description("List channels in a server").requiredOption("--server-id <id>", "Server ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
389
549
|
try {
|
|
390
550
|
const client = await getClient(options.profile);
|
|
@@ -572,9 +732,9 @@ function createChannelsCommand() {
|
|
|
572
732
|
|
|
573
733
|
// src/commands/cloud.ts
|
|
574
734
|
import { execFileSync, spawnSync } from "child_process";
|
|
575
|
-
import { Command as
|
|
735
|
+
import { Command as Command6 } from "commander";
|
|
576
736
|
function createCloudCommand() {
|
|
577
|
-
const cloud = new
|
|
737
|
+
const cloud = new Command6("cloud").description("Shadow Cloud \u2014 deploy AI agent clusters to Kubernetes (via shadowob-cloud)").allowUnknownOption(true).allowExcessArguments(true).action(async (_, cmd) => {
|
|
578
738
|
const args = cmd.args ?? [];
|
|
579
739
|
ensureCloudCliInstalled();
|
|
580
740
|
spawnCloudCli(args);
|
|
@@ -597,10 +757,217 @@ function spawnCloudCli(args) {
|
|
|
597
757
|
}
|
|
598
758
|
}
|
|
599
759
|
|
|
760
|
+
// src/commands/commerce.ts
|
|
761
|
+
import { randomUUID } from "crypto";
|
|
762
|
+
import { Command as Command7 } from "commander";
|
|
763
|
+
function buildIdempotencyKey(value, prefix) {
|
|
764
|
+
return value?.trim() || `${prefix}-${randomUUID()}`;
|
|
765
|
+
}
|
|
766
|
+
function parsePositiveInteger(value, name) {
|
|
767
|
+
if (value == null) return void 0;
|
|
768
|
+
const parsed = Number.parseInt(value, 10);
|
|
769
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
770
|
+
throw new Error(`${name} must be a positive integer`);
|
|
771
|
+
}
|
|
772
|
+
return parsed;
|
|
773
|
+
}
|
|
774
|
+
function parseMetadata(value) {
|
|
775
|
+
if (!value) return void 0;
|
|
776
|
+
const parsed = JSON.parse(value);
|
|
777
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
778
|
+
throw new Error("--metadata must be a JSON object");
|
|
779
|
+
}
|
|
780
|
+
return parsed;
|
|
781
|
+
}
|
|
782
|
+
async function runCommand(options, task) {
|
|
783
|
+
try {
|
|
784
|
+
const result = await task();
|
|
785
|
+
output(result, { json: options.json });
|
|
786
|
+
} catch (error) {
|
|
787
|
+
outputError(error instanceof Error ? error.message : String(error), { json: options.json });
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function createCommerceCommand() {
|
|
792
|
+
const commerce = new Command7("commerce").description("Commerce, purchases, delivery, and assets");
|
|
793
|
+
const products = commerce.command("products").description("Buyer-facing product commands");
|
|
794
|
+
products.command("context").description("Get buyer-facing product context").argument("<product-id>", "Product ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
795
|
+
(productId, options) => runCommand(options, async () => {
|
|
796
|
+
const client = await getClient(options.profile);
|
|
797
|
+
return client.getCommerceProductContext(productId);
|
|
798
|
+
})
|
|
799
|
+
);
|
|
800
|
+
const offers = commerce.command("offers").description("Commerce offer commands");
|
|
801
|
+
offers.command("preview").description("Preview checkout for an offer").argument("<offer-id>", "Offer ID").option("--sku-id <id>", "SKU ID").option("--viewer-user-id <id>", "Viewer user ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
802
|
+
(offerId, options) => runCommand(options, async () => {
|
|
803
|
+
const client = await getClient(options.profile);
|
|
804
|
+
return client.getCommerceOfferCheckoutPreview(offerId, {
|
|
805
|
+
skuId: options.skuId,
|
|
806
|
+
viewerUserId: options.viewerUserId
|
|
807
|
+
});
|
|
808
|
+
})
|
|
809
|
+
);
|
|
810
|
+
offers.command("purchase").description("Purchase an offer").argument("<offer-id>", "Offer ID").option("--sku-id <id>", "SKU ID").option("--destination-kind <kind>", "Delivery destination kind, currently channel").option("--destination-id <id>", "Delivery destination ID").option("--idempotency-key <key>", "Idempotency key, generated if omitted").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
811
|
+
(offerId, options) => runCommand(options, async () => {
|
|
812
|
+
const client = await getClient(options.profile);
|
|
813
|
+
return client.purchaseCommerceOffer(offerId, {
|
|
814
|
+
skuId: options.skuId,
|
|
815
|
+
idempotencyKey: buildIdempotencyKey(options.idempotencyKey, "cli-offer-purchase"),
|
|
816
|
+
destinationKind: options.destinationId ? options.destinationKind ?? "channel" : void 0,
|
|
817
|
+
destinationId: options.destinationId
|
|
818
|
+
});
|
|
819
|
+
})
|
|
820
|
+
);
|
|
821
|
+
const cards = commerce.command("cards").description("Chat commerce card commands");
|
|
822
|
+
cards.command("list").description("List commerce product cards available for a channel").requiredOption("--channel-id <id>", "Channel ID").option("--keyword <keyword>", "Search keyword").option("--limit <n>", "Maximum cards").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
823
|
+
(options) => runCommand(options, async () => {
|
|
824
|
+
const client = await getClient(options.profile);
|
|
825
|
+
return client.listCommerceProductCards({
|
|
826
|
+
target: "channel",
|
|
827
|
+
channelId: options.channelId,
|
|
828
|
+
keyword: options.keyword,
|
|
829
|
+
limit: parsePositiveInteger(options.limit, "--limit")
|
|
830
|
+
});
|
|
831
|
+
})
|
|
832
|
+
);
|
|
833
|
+
cards.command("purchase").description("Purchase a chat commerce card").argument("<message-id>", "Message ID").argument("<card-id>", "Commerce card ID").option("--sku-id <id>", "SKU ID").option("--idempotency-key <key>", "Idempotency key, generated if omitted").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
834
|
+
(messageId, cardId, options) => runCommand(options, async () => {
|
|
835
|
+
const client = await getClient(options.profile);
|
|
836
|
+
return client.purchaseMessageCommerceCard(messageId, cardId, {
|
|
837
|
+
skuId: options.skuId,
|
|
838
|
+
idempotencyKey: buildIdempotencyKey(options.idempotencyKey, "cli-card-purchase")
|
|
839
|
+
});
|
|
840
|
+
})
|
|
841
|
+
);
|
|
842
|
+
const entitlements = commerce.command("entitlements").description("Purchase entitlement commands");
|
|
843
|
+
entitlements.command("list").description("List my purchase entitlements").option("--server-id <id>", "Limit to a server shop entitlement list").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
844
|
+
(options) => runCommand(options, async () => {
|
|
845
|
+
const client = await getClient(options.profile);
|
|
846
|
+
return options.serverId ? client.getEntitlements(options.serverId) : client.getAllEntitlements();
|
|
847
|
+
})
|
|
848
|
+
);
|
|
849
|
+
entitlements.command("get").description("Get purchase delivery detail").argument("<entitlement-id>", "Entitlement ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
850
|
+
(entitlementId, options) => runCommand(options, async () => {
|
|
851
|
+
const client = await getClient(options.profile);
|
|
852
|
+
return client.getEntitlement(entitlementId);
|
|
853
|
+
})
|
|
854
|
+
);
|
|
855
|
+
entitlements.command("verify").description("Verify entitlement provisioning/access").argument("<entitlement-id>", "Entitlement ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
856
|
+
(entitlementId, options) => runCommand(options, async () => {
|
|
857
|
+
const client = await getClient(options.profile);
|
|
858
|
+
return client.verifyEntitlement(entitlementId);
|
|
859
|
+
})
|
|
860
|
+
);
|
|
861
|
+
entitlements.command("cancel").description("Cancel an entitlement and request any available refund").argument("<entitlement-id>", "Entitlement ID").option("--reason <reason>", "Cancellation reason").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
862
|
+
(entitlementId, options) => runCommand(options, async () => {
|
|
863
|
+
const client = await getClient(options.profile);
|
|
864
|
+
return client.cancelEntitlement(entitlementId, options.reason);
|
|
865
|
+
})
|
|
866
|
+
);
|
|
867
|
+
entitlements.command("cancel-renewal").description("Stop subscription renewal while keeping current access").argument("<entitlement-id>", "Entitlement ID").option("--reason <reason>", "Cancellation reason").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
868
|
+
(entitlementId, options) => runCommand(options, async () => {
|
|
869
|
+
const client = await getClient(options.profile);
|
|
870
|
+
return client.cancelEntitlementRenewal(entitlementId, options.reason);
|
|
871
|
+
})
|
|
872
|
+
);
|
|
873
|
+
const assets = commerce.command("assets").description("Community asset commands");
|
|
874
|
+
assets.command("list").description("List my community assets").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
875
|
+
(options) => runCommand(options, async () => {
|
|
876
|
+
const client = await getClient(options.profile);
|
|
877
|
+
return client.listCommunityAssets();
|
|
878
|
+
})
|
|
879
|
+
);
|
|
880
|
+
assets.command("get").description("Get a community asset grant").argument("<grant-id>", "Asset grant ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
881
|
+
(grantId, options) => runCommand(options, async () => {
|
|
882
|
+
const client = await getClient(options.profile);
|
|
883
|
+
return client.getCommunityAsset(grantId);
|
|
884
|
+
})
|
|
885
|
+
);
|
|
886
|
+
for (const action of ["consume", "lock", "unlock"]) {
|
|
887
|
+
assets.command(action).description(`${action} a community asset grant`).argument("<grant-id>", "Asset grant ID").option("--idempotency-key <key>", "Idempotency key, generated if omitted").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
888
|
+
(grantId, options) => runCommand(options, async () => {
|
|
889
|
+
const client = await getClient(options.profile);
|
|
890
|
+
const data = {
|
|
891
|
+
idempotencyKey: buildIdempotencyKey(options.idempotencyKey, `cli-asset-${action}`)
|
|
892
|
+
};
|
|
893
|
+
if (action === "consume") return client.consumeCommunityAsset(grantId, data);
|
|
894
|
+
if (action === "lock") return client.lockCommunityAsset(grantId, data);
|
|
895
|
+
return client.unlockCommunityAsset(grantId, data);
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
assets.command("revoke").description("Revoke a community asset grant").argument("<grant-id>", "Asset grant ID").option("--reason <reason>", "Revocation reason").option("--idempotency-key <key>", "Idempotency key, generated if omitted").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
900
|
+
(grantId, options) => runCommand(options, async () => {
|
|
901
|
+
const client = await getClient(options.profile);
|
|
902
|
+
return client.revokeCommunityAsset(grantId, {
|
|
903
|
+
reason: options.reason,
|
|
904
|
+
idempotencyKey: buildIdempotencyKey(options.idempotencyKey, "cli-asset-revoke")
|
|
905
|
+
});
|
|
906
|
+
})
|
|
907
|
+
);
|
|
908
|
+
const paidFiles = commerce.command("paid-files").description("Protected paid file commands");
|
|
909
|
+
paidFiles.command("open").description("Open a paid file with entitlement authorization").argument("<file-id>", "Paid file/workspace file ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
910
|
+
(fileId, options) => runCommand(options, async () => {
|
|
911
|
+
const client = await getClient(options.profile);
|
|
912
|
+
return client.openPaidFile(fileId);
|
|
913
|
+
})
|
|
914
|
+
);
|
|
915
|
+
const settlements = commerce.command("settlements").description("Settlement commands");
|
|
916
|
+
settlements.command("list").description("List settlement lines").option("--limit <n>", "Maximum settlement lines").option("--offset <n>", "Offset").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
917
|
+
(options) => runCommand(options, async () => {
|
|
918
|
+
const client = await getClient(options.profile);
|
|
919
|
+
return client.listSettlements({
|
|
920
|
+
limit: parsePositiveInteger(options.limit, "--limit"),
|
|
921
|
+
offset: options.offset ? Number.parseInt(options.offset, 10) : void 0
|
|
922
|
+
});
|
|
923
|
+
})
|
|
924
|
+
);
|
|
925
|
+
settlements.command("settle").description("Settle currently available settlement lines").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
926
|
+
(options) => runCommand(options, async () => {
|
|
927
|
+
const client = await getClient(options.profile);
|
|
928
|
+
return client.settleAvailableSettlements();
|
|
929
|
+
})
|
|
930
|
+
);
|
|
931
|
+
const tips = commerce.command("tips").description("Tip commands");
|
|
932
|
+
tips.command("send").description("Send a tip").requiredOption("--recipient-user-id <id>", "Recipient user ID").requiredOption("--amount <amount>", "Amount").option("--message <message>", "Message").option("--context <json>", "Context JSON object").option("--idempotency-key <key>", "Idempotency key, generated if omitted").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
933
|
+
(options) => runCommand(options, async () => {
|
|
934
|
+
const client = await getClient(options.profile);
|
|
935
|
+
const amount = parsePositiveInteger(options.amount, "--amount");
|
|
936
|
+
return client.sendTip({
|
|
937
|
+
recipientUserId: options.recipientUserId,
|
|
938
|
+
amount: amount ?? 0,
|
|
939
|
+
message: options.message,
|
|
940
|
+
context: parseMetadata(options.context),
|
|
941
|
+
idempotencyKey: buildIdempotencyKey(options.idempotencyKey, "cli-tip")
|
|
942
|
+
});
|
|
943
|
+
})
|
|
944
|
+
);
|
|
945
|
+
const gifts = commerce.command("gifts").description("Gift commands");
|
|
946
|
+
gifts.command("list").description("List gifts").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
947
|
+
(options) => runCommand(options, async () => {
|
|
948
|
+
const client = await getClient(options.profile);
|
|
949
|
+
return client.listGifts();
|
|
950
|
+
})
|
|
951
|
+
);
|
|
952
|
+
gifts.command("send").description("Send a gift").requiredOption("--recipient-user-id <id>", "Recipient user ID").option("--assets <json>", "Assets JSON array").option("--currencies <json>", "Currencies JSON array").option("--message <message>", "Message").option("--idempotency-key <key>", "Idempotency key, generated if omitted").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
953
|
+
(options) => runCommand(options, async () => {
|
|
954
|
+
const client = await getClient(options.profile);
|
|
955
|
+
return client.sendGift({
|
|
956
|
+
recipientUserId: options.recipientUserId,
|
|
957
|
+
assets: options.assets ? JSON.parse(options.assets) : void 0,
|
|
958
|
+
currencies: options.currencies ? JSON.parse(options.currencies) : void 0,
|
|
959
|
+
message: options.message,
|
|
960
|
+
idempotencyKey: buildIdempotencyKey(options.idempotencyKey, "cli-gift")
|
|
961
|
+
});
|
|
962
|
+
})
|
|
963
|
+
);
|
|
964
|
+
return commerce;
|
|
965
|
+
}
|
|
966
|
+
|
|
600
967
|
// src/commands/config.ts
|
|
601
|
-
import { Command as
|
|
968
|
+
import { Command as Command8 } from "commander";
|
|
602
969
|
function createConfigCommand() {
|
|
603
|
-
const config = new
|
|
970
|
+
const config = new Command8("config").description("Configuration management commands");
|
|
604
971
|
config.command("path").description("Show configuration file path").action(() => {
|
|
605
972
|
console.log(configManager.getConfigPath());
|
|
606
973
|
});
|
|
@@ -671,9 +1038,9 @@ function createConfigCommand() {
|
|
|
671
1038
|
}
|
|
672
1039
|
|
|
673
1040
|
// src/commands/discover.ts
|
|
674
|
-
import { Command as
|
|
1041
|
+
import { Command as Command9 } from "commander";
|
|
675
1042
|
function createDiscoverCommand() {
|
|
676
|
-
const discover = new
|
|
1043
|
+
const discover = new Command9("discover").description("Discover popular servers and channels");
|
|
677
1044
|
discover.command("feed").description("Get the discovery feed").option("--type <type>", "Filter by type (all, servers, channels, rentals)", "all").option("--limit <n>", "Number of results", "20").option("--offset <n>", "Offset for pagination", "0").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
678
1045
|
try {
|
|
679
1046
|
const client = await getClient(options.profile);
|
|
@@ -710,9 +1077,9 @@ function createDiscoverCommand() {
|
|
|
710
1077
|
}
|
|
711
1078
|
|
|
712
1079
|
// src/commands/dms.ts
|
|
713
|
-
import { Command as
|
|
1080
|
+
import { Command as Command10 } from "commander";
|
|
714
1081
|
function createDirectMessagesCommand() {
|
|
715
|
-
const dms = new
|
|
1082
|
+
const dms = new Command10("dms").description("Direct message commands");
|
|
716
1083
|
dms.command("list").description("List direct channels").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
717
1084
|
try {
|
|
718
1085
|
const client = await getClient(options.profile);
|
|
@@ -790,9 +1157,9 @@ function createDirectMessagesCommand() {
|
|
|
790
1157
|
}
|
|
791
1158
|
|
|
792
1159
|
// src/commands/friends.ts
|
|
793
|
-
import { Command as
|
|
1160
|
+
import { Command as Command11 } from "commander";
|
|
794
1161
|
function createFriendsCommand() {
|
|
795
|
-
const friends = new
|
|
1162
|
+
const friends = new Command11("friends").description("Friendship management commands");
|
|
796
1163
|
friends.command("list").description("List friends").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
797
1164
|
try {
|
|
798
1165
|
const client = await getClient(options.profile);
|
|
@@ -876,9 +1243,9 @@ function createFriendsCommand() {
|
|
|
876
1243
|
}
|
|
877
1244
|
|
|
878
1245
|
// src/commands/invites.ts
|
|
879
|
-
import { Command as
|
|
1246
|
+
import { Command as Command12 } from "commander";
|
|
880
1247
|
function createInvitesCommand() {
|
|
881
|
-
const invites = new
|
|
1248
|
+
const invites = new Command12("invites").description("Invite code management commands");
|
|
882
1249
|
invites.command("list").description("List your invite codes").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
883
1250
|
try {
|
|
884
1251
|
const client = await getClient(options.profile);
|
|
@@ -929,15 +1296,15 @@ function createInvitesCommand() {
|
|
|
929
1296
|
}
|
|
930
1297
|
|
|
931
1298
|
// src/commands/listen.ts
|
|
932
|
-
import { Command as
|
|
1299
|
+
import { Command as Command13 } from "commander";
|
|
933
1300
|
function createListenCommand() {
|
|
934
|
-
const listen = new
|
|
1301
|
+
const listen = new Command13("listen").description("Listen to real-time events");
|
|
935
1302
|
listen.command("channel").description("Listen to events in a channel").argument("<channel-id>", "Channel ID").option("--mode <mode>", "Listen mode: stream or poll", "stream").option("--timeout <seconds>", "Timeout in seconds (stream mode)", "60").option("--count <n>", "Stop after N events (stream mode)").option("--since <duration>", "Poll events since duration (e.g., 5m, 1h)", "5m").option("--last <n>", "Poll last N messages", "50").option("--event-type <type>", "Filter by event type (comma-separated)").option("--profile <name>", "Profile to use").option("--json", "Output as JSON (one per line)").action(
|
|
936
1303
|
async (channelId, options) => {
|
|
937
1304
|
try {
|
|
938
1305
|
const eventTypes = options.eventType?.split(",").map((t) => t.trim());
|
|
939
1306
|
if (options.mode === "poll") {
|
|
940
|
-
const { getClient: getClient2 } = await import("./client-
|
|
1307
|
+
const { getClient: getClient2 } = await import("./client-ZIUDIQPZ.js");
|
|
941
1308
|
const client = await getClient2(options.profile);
|
|
942
1309
|
const limit = parseInt(options.last ?? "50", 10);
|
|
943
1310
|
const result = await client.getMessages(channelId, limit);
|
|
@@ -1051,9 +1418,9 @@ function createListenCommand() {
|
|
|
1051
1418
|
}
|
|
1052
1419
|
|
|
1053
1420
|
// src/commands/marketplace.ts
|
|
1054
|
-
import { Command as
|
|
1421
|
+
import { Command as Command14 } from "commander";
|
|
1055
1422
|
function createMarketplaceCommand() {
|
|
1056
|
-
const marketplace = new
|
|
1423
|
+
const marketplace = new Command14("marketplace").description("Marketplace commands");
|
|
1057
1424
|
const listings = marketplace.command("listings").description("Listing commands");
|
|
1058
1425
|
listings.command("list").description("Browse marketplace listings").option("--search <text>", "Search query").option("--tags <tags>", "Comma-separated tags").option("--min-price <n>", "Minimum price per hour").option("--max-price <n>", "Maximum price per hour").option("--limit <n>", "Number of results", "20").option("--offset <n>", "Pagination offset", "0").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
1059
1426
|
async (options) => {
|
|
@@ -1198,9 +1565,9 @@ function createMarketplaceCommand() {
|
|
|
1198
1565
|
|
|
1199
1566
|
// src/commands/media.ts
|
|
1200
1567
|
import { readFileSync } from "fs";
|
|
1201
|
-
import { Command as
|
|
1568
|
+
import { Command as Command15 } from "commander";
|
|
1202
1569
|
function createMediaCommand() {
|
|
1203
|
-
const media = new
|
|
1570
|
+
const media = new Command15("media").description("Media management commands");
|
|
1204
1571
|
media.command("upload").description("Upload a file").requiredOption("--file <path>", "File path to upload").option("--message-id <id>", "Associate with message").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
1205
1572
|
async (options) => {
|
|
1206
1573
|
try {
|
|
@@ -1266,16 +1633,18 @@ function createMediaCommand() {
|
|
|
1266
1633
|
}
|
|
1267
1634
|
|
|
1268
1635
|
// src/commands/notifications.ts
|
|
1269
|
-
import { Command as
|
|
1636
|
+
import { Command as Command16 } from "commander";
|
|
1270
1637
|
function createNotificationsCommand() {
|
|
1271
|
-
const notifications = new
|
|
1638
|
+
const notifications = new Command16("notifications").description("Notification commands");
|
|
1272
1639
|
notifications.command("list").description("List notifications").option("--unread-only", "Show only unread notifications").option("--limit <n>", "Number of notifications", "20").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
1273
1640
|
async (options) => {
|
|
1274
1641
|
try {
|
|
1275
1642
|
const client = await getClient(options.profile);
|
|
1276
1643
|
const limit = parseInt(options.limit ?? "20", 10);
|
|
1277
1644
|
const result = await client.listNotifications(limit);
|
|
1278
|
-
const notifications2 = Array.isArray(result) ? result : []
|
|
1645
|
+
const notifications2 = (Array.isArray(result) ? result : []).filter(
|
|
1646
|
+
(item) => !options.unreadOnly || item.isRead === false
|
|
1647
|
+
);
|
|
1279
1648
|
output(notifications2, { json: options.json });
|
|
1280
1649
|
} catch (error) {
|
|
1281
1650
|
outputError(error instanceof Error ? error.message : String(error), {
|
|
@@ -1307,13 +1676,62 @@ function createNotificationsCommand() {
|
|
|
1307
1676
|
process.exit(1);
|
|
1308
1677
|
}
|
|
1309
1678
|
});
|
|
1679
|
+
const preferences = notifications.command("preferences").description("Notification preferences");
|
|
1680
|
+
preferences.command("get").description("Get notification preferences").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
1681
|
+
try {
|
|
1682
|
+
const client = await getClient(options.profile);
|
|
1683
|
+
output(await client.getNotificationPreferences(), { json: options.json });
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
outputError(error instanceof Error ? error.message : String(error), { json: options.json });
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
preferences.command("update").description("Update notification preferences").option("--strategy <strategy>", "all | mention_only | none").option("--muted-server-ids <ids>", "Comma-separated server IDs").option("--muted-channel-ids <ids>", "Comma-separated channel IDs").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
1690
|
+
async (options) => {
|
|
1691
|
+
try {
|
|
1692
|
+
if (options.strategy && !["all", "mention_only", "none"].includes(options.strategy)) {
|
|
1693
|
+
throw new Error("Invalid --strategy. Expected all, mention_only, or none");
|
|
1694
|
+
}
|
|
1695
|
+
const client = await getClient(options.profile);
|
|
1696
|
+
const data = {
|
|
1697
|
+
...options.strategy ? { strategy: options.strategy } : {},
|
|
1698
|
+
...options.mutedServerIds !== void 0 ? { mutedServerIds: splitIds(options.mutedServerIds) } : {},
|
|
1699
|
+
...options.mutedChannelIds !== void 0 ? { mutedChannelIds: splitIds(options.mutedChannelIds) } : {}
|
|
1700
|
+
};
|
|
1701
|
+
output(await client.updateNotificationPreferences(data), { json: options.json });
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
1704
|
+
json: options.json
|
|
1705
|
+
});
|
|
1706
|
+
process.exit(1);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
);
|
|
1310
1710
|
return notifications;
|
|
1311
1711
|
}
|
|
1712
|
+
function splitIds(value) {
|
|
1713
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1714
|
+
}
|
|
1312
1715
|
|
|
1313
1716
|
// src/commands/oauth.ts
|
|
1314
|
-
import { Command as
|
|
1717
|
+
import { Command as Command17 } from "commander";
|
|
1718
|
+
function resolveOAuthAccessToken(options) {
|
|
1719
|
+
const token = options.accessToken || process.env.SHADOWOB_OAUTH_TOKEN;
|
|
1720
|
+
if (!token) {
|
|
1721
|
+
throw new Error("Provide --access-token or SHADOWOB_OAUTH_TOKEN for OAuth commerce APIs");
|
|
1722
|
+
}
|
|
1723
|
+
return token;
|
|
1724
|
+
}
|
|
1725
|
+
function parseMetadata2(value) {
|
|
1726
|
+
if (!value) return void 0;
|
|
1727
|
+
const parsed = JSON.parse(value);
|
|
1728
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1729
|
+
throw new Error("--metadata must be a JSON object");
|
|
1730
|
+
}
|
|
1731
|
+
return parsed;
|
|
1732
|
+
}
|
|
1315
1733
|
function createOAuthCommand() {
|
|
1316
|
-
const oauth = new
|
|
1734
|
+
const oauth = new Command17("oauth").description("OAuth management commands");
|
|
1317
1735
|
oauth.command("list").description("List OAuth apps").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
1318
1736
|
try {
|
|
1319
1737
|
const client = await getClient(options.profile);
|
|
@@ -1403,13 +1821,52 @@ function createOAuthCommand() {
|
|
|
1403
1821
|
process.exit(1);
|
|
1404
1822
|
}
|
|
1405
1823
|
});
|
|
1824
|
+
const commerce = oauth.command("commerce").description("OAuth commerce entitlement commands");
|
|
1825
|
+
commerce.command("check").description("Check the OAuth token user entitlement for the calling app").option("--resource-type <type>", "Resource type, defaults to external_app").option("--resource-id <id>", "App resource ID or app-id:feature").option("--capability <capability>", "Capability, defaults to use").option("--access-token <token>", "OAuth access token; defaults to SHADOWOB_OAUTH_TOKEN").option("--profile <name>", "Profile to use for server URL").option("--json", "Output as JSON").action(
|
|
1826
|
+
async (options) => {
|
|
1827
|
+
try {
|
|
1828
|
+
const client = await getClientWithToken(resolveOAuthAccessToken(options), options.profile);
|
|
1829
|
+
const result = await client.getOAuthCommerceEntitlementAccess({
|
|
1830
|
+
resourceType: options.resourceType,
|
|
1831
|
+
resourceId: options.resourceId,
|
|
1832
|
+
capability: options.capability
|
|
1833
|
+
});
|
|
1834
|
+
output(result, { json: options.json });
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
1837
|
+
json: options.json
|
|
1838
|
+
});
|
|
1839
|
+
process.exit(1);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
);
|
|
1843
|
+
commerce.command("redeem").description("Redeem the OAuth token user entitlement for the calling app").requiredOption("--idempotency-key <key>", "Provider idempotency key").option("--resource-type <type>", "Resource type, defaults to external_app").option("--resource-id <id>", "App resource ID or app-id:feature").option("--capability <capability>", "Capability, defaults to use").option("--metadata <json>", "Flat provider metadata JSON object").option("--access-token <token>", "OAuth access token; defaults to SHADOWOB_OAUTH_TOKEN").option("--profile <name>", "Profile to use for server URL").option("--json", "Output as JSON").action(
|
|
1844
|
+
async (options) => {
|
|
1845
|
+
try {
|
|
1846
|
+
const client = await getClientWithToken(resolveOAuthAccessToken(options), options.profile);
|
|
1847
|
+
const result = await client.redeemOAuthCommerceEntitlement({
|
|
1848
|
+
idempotencyKey: options.idempotencyKey,
|
|
1849
|
+
resourceType: options.resourceType,
|
|
1850
|
+
resourceId: options.resourceId,
|
|
1851
|
+
capability: options.capability,
|
|
1852
|
+
metadata: parseMetadata2(options.metadata)
|
|
1853
|
+
});
|
|
1854
|
+
output(result, { json: options.json });
|
|
1855
|
+
} catch (error) {
|
|
1856
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
1857
|
+
json: options.json
|
|
1858
|
+
});
|
|
1859
|
+
process.exit(1);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
);
|
|
1406
1863
|
return oauth;
|
|
1407
1864
|
}
|
|
1408
1865
|
|
|
1409
1866
|
// src/commands/ping.ts
|
|
1410
|
-
import { Command as
|
|
1867
|
+
import { Command as Command18 } from "commander";
|
|
1411
1868
|
function createPingCommand() {
|
|
1412
|
-
const ping = new
|
|
1869
|
+
const ping = new Command18("ping").description("Test connection to Shadow server");
|
|
1413
1870
|
ping.option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
1414
1871
|
const startTime = Date.now();
|
|
1415
1872
|
const outputOpts = { json: options.json };
|
|
@@ -1458,9 +1915,9 @@ function createPingCommand() {
|
|
|
1458
1915
|
}
|
|
1459
1916
|
|
|
1460
1917
|
// src/commands/profile-comments.ts
|
|
1461
|
-
import { Command as
|
|
1918
|
+
import { Command as Command19 } from "commander";
|
|
1462
1919
|
function createProfileCommentsCommand() {
|
|
1463
|
-
const comments = new
|
|
1920
|
+
const comments = new Command19("profile-comments").description(
|
|
1464
1921
|
"Profile comment management commands"
|
|
1465
1922
|
);
|
|
1466
1923
|
comments.command("get").description("Get comments for a user profile").argument("<user-id>", "Profile user ID").option("--limit <n>", "Number of results", "20").option("--offset <n>", "Offset for pagination", "0").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (userId, options) => {
|
|
@@ -1510,9 +1967,9 @@ function createProfileCommentsCommand() {
|
|
|
1510
1967
|
}
|
|
1511
1968
|
|
|
1512
1969
|
// src/commands/search.ts
|
|
1513
|
-
import { Command as
|
|
1970
|
+
import { Command as Command20 } from "commander";
|
|
1514
1971
|
function createSearchCommand() {
|
|
1515
|
-
const search = new
|
|
1972
|
+
const search = new Command20("search").description("Search commands");
|
|
1516
1973
|
search.command("messages").description("Search messages").requiredOption("--query <text>", "Search query").option("--server-id <id>", "Limit to server").option("--channel-id <id>", "Limit to channel").option("--limit <n>", "Number of results (1-100)", "20").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
1517
1974
|
async (options) => {
|
|
1518
1975
|
try {
|
|
@@ -1537,9 +1994,9 @@ function createSearchCommand() {
|
|
|
1537
1994
|
}
|
|
1538
1995
|
|
|
1539
1996
|
// src/commands/servers.ts
|
|
1540
|
-
import { Command as
|
|
1997
|
+
import { Command as Command21 } from "commander";
|
|
1541
1998
|
function createServersCommand() {
|
|
1542
|
-
const servers = new
|
|
1999
|
+
const servers = new Command21("servers").description("Server management commands");
|
|
1543
2000
|
servers.command("list").description("List all servers you have joined").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
1544
2001
|
try {
|
|
1545
2002
|
const client = await getClient(options.profile);
|
|
@@ -1619,45 +2076,6 @@ function createServersCommand() {
|
|
|
1619
2076
|
process.exit(1);
|
|
1620
2077
|
}
|
|
1621
2078
|
});
|
|
1622
|
-
servers.command("homepage").description("Get or set server homepage").argument("<server-id>", "Server ID or slug").option("--set <file>", 'Set homepage from HTML file (use "-" for stdin)').option("--clear", "Clear homepage (reset to default)").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
1623
|
-
async (serverId, options) => {
|
|
1624
|
-
try {
|
|
1625
|
-
const client = await getClient(options.profile);
|
|
1626
|
-
if (options.clear) {
|
|
1627
|
-
const result = await client.updateServerHomepage(serverId, null);
|
|
1628
|
-
output(result, { json: options.json });
|
|
1629
|
-
return;
|
|
1630
|
-
}
|
|
1631
|
-
if (options.set) {
|
|
1632
|
-
let html;
|
|
1633
|
-
if (options.set === "-") {
|
|
1634
|
-
const chunks = [];
|
|
1635
|
-
for await (const chunk of process.stdin) {
|
|
1636
|
-
chunks.push(Buffer.from(chunk));
|
|
1637
|
-
}
|
|
1638
|
-
html = Buffer.concat(chunks).toString("utf-8");
|
|
1639
|
-
} else {
|
|
1640
|
-
const { readFile: readFile2 } = await import("fs/promises");
|
|
1641
|
-
html = await readFile2(options.set, "utf-8");
|
|
1642
|
-
}
|
|
1643
|
-
const result = await client.updateServerHomepage(serverId, html);
|
|
1644
|
-
output(result, { json: options.json });
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
const server = await client.getServer(serverId);
|
|
1648
|
-
if (options.json) {
|
|
1649
|
-
output({ homepageHtml: server.homepageHtml }, { json: true });
|
|
1650
|
-
} else {
|
|
1651
|
-
console.log(server.homepageHtml ?? "(default homepage)");
|
|
1652
|
-
}
|
|
1653
|
-
} catch (error) {
|
|
1654
|
-
outputError(error instanceof Error ? error.message : String(error), {
|
|
1655
|
-
json: options.json
|
|
1656
|
-
});
|
|
1657
|
-
process.exit(1);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
);
|
|
1661
2079
|
servers.command("discover").description("Discover public servers").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
1662
2080
|
try {
|
|
1663
2081
|
const client = await getClient(options.profile);
|
|
@@ -1672,9 +2090,25 @@ function createServersCommand() {
|
|
|
1672
2090
|
}
|
|
1673
2091
|
|
|
1674
2092
|
// src/commands/shop.ts
|
|
1675
|
-
import { Command as
|
|
2093
|
+
import { Command as Command22 } from "commander";
|
|
2094
|
+
function parseJsonObject(value, optionName) {
|
|
2095
|
+
if (!value) return {};
|
|
2096
|
+
const parsed = JSON.parse(value);
|
|
2097
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2098
|
+
throw new Error(`${optionName} must be a JSON object`);
|
|
2099
|
+
}
|
|
2100
|
+
return parsed;
|
|
2101
|
+
}
|
|
2102
|
+
function parseOptionalNumber(value, optionName) {
|
|
2103
|
+
if (value == null) return void 0;
|
|
2104
|
+
const parsed = Number.parseInt(value, 10);
|
|
2105
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
2106
|
+
throw new Error(`${optionName} must be a positive integer`);
|
|
2107
|
+
}
|
|
2108
|
+
return parsed;
|
|
2109
|
+
}
|
|
1676
2110
|
function createShopCommand() {
|
|
1677
|
-
const shop = new
|
|
2111
|
+
const shop = new Command22("shop").description("Shop commands");
|
|
1678
2112
|
shop.command("get").description("Get shop info").argument("<server-id>", "Server ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (serverId, options) => {
|
|
1679
2113
|
try {
|
|
1680
2114
|
const client = await getClient(options.profile);
|
|
@@ -1685,17 +2119,77 @@ function createShopCommand() {
|
|
|
1685
2119
|
process.exit(1);
|
|
1686
2120
|
}
|
|
1687
2121
|
});
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2122
|
+
shop.command("get-by-id").description("Get shop info by shop ID").argument("<shop-id>", "Shop ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (shopId, options) => {
|
|
2123
|
+
try {
|
|
2124
|
+
const client = await getClient(options.profile);
|
|
2125
|
+
const shopData = await client.getShopById(shopId);
|
|
2126
|
+
output(shopData, { json: options.json });
|
|
2127
|
+
} catch (error) {
|
|
2128
|
+
outputError(error instanceof Error ? error.message : String(error), { json: options.json });
|
|
2129
|
+
process.exit(1);
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
const me = shop.command("me").description("Personal shop commands");
|
|
2133
|
+
me.command("get").description("Get my personal shop").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
2134
|
+
try {
|
|
2135
|
+
const client = await getClient(options.profile);
|
|
2136
|
+
output(await client.getMyShop(), { json: options.json });
|
|
2137
|
+
} catch (error) {
|
|
2138
|
+
outputError(error instanceof Error ? error.message : String(error), { json: options.json });
|
|
2139
|
+
process.exit(1);
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
me.command("upsert").description("Create or update my personal shop").option("--data <json>", "Shop JSON payload").option("--name <name>", "Shop name").option("--description <description>", "Shop description").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2143
|
+
async (options) => {
|
|
2144
|
+
try {
|
|
2145
|
+
const client = await getClient(options.profile);
|
|
2146
|
+
const payload = parseJsonObject(options.data, "--data");
|
|
2147
|
+
if (options.name) payload.name = options.name;
|
|
2148
|
+
if (options.description) payload.description = options.description;
|
|
2149
|
+
output(await client.upsertMyShop(payload), { json: options.json });
|
|
2150
|
+
} catch (error) {
|
|
2151
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2152
|
+
json: options.json
|
|
2153
|
+
});
|
|
2154
|
+
process.exit(1);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
);
|
|
2158
|
+
const products = shop.command("products").description("Product commands");
|
|
2159
|
+
products.command("list").description("List products").argument("<server-id>", "Server ID").option("--category-id <id>", "Filter by category").option("--status <status>", "Filter by status").option("--keyword <keyword>", "Search keyword").option("--limit <n>", "Maximum products").option("--offset <n>", "Offset").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2160
|
+
async (serverId, options) => {
|
|
2161
|
+
try {
|
|
2162
|
+
const client = await getClient(options.profile);
|
|
2163
|
+
const products2 = await client.listProducts(serverId, {
|
|
2164
|
+
categoryId: options.categoryId,
|
|
2165
|
+
status: options.status,
|
|
2166
|
+
keyword: options.keyword,
|
|
2167
|
+
limit: parseOptionalNumber(options.limit, "--limit"),
|
|
2168
|
+
offset: options.offset ? Number.parseInt(options.offset, 10) : void 0
|
|
2169
|
+
});
|
|
2170
|
+
output(products2, { json: options.json });
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2173
|
+
json: options.json
|
|
2174
|
+
});
|
|
2175
|
+
process.exit(1);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
);
|
|
2179
|
+
products.command("list-by-shop").description("List products by shop ID").argument("<shop-id>", "Shop ID").option("--keyword <keyword>", "Search keyword").option("--limit <n>", "Maximum products").option("--offset <n>", "Offset").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2180
|
+
async (shopId, options) => {
|
|
2181
|
+
try {
|
|
2182
|
+
const client = await getClient(options.profile);
|
|
2183
|
+
const result = await client.listShopProducts(shopId, {
|
|
2184
|
+
keyword: options.keyword,
|
|
2185
|
+
limit: parseOptionalNumber(options.limit, "--limit"),
|
|
2186
|
+
offset: options.offset ? Number.parseInt(options.offset, 10) : void 0
|
|
2187
|
+
});
|
|
2188
|
+
output(result, { json: options.json });
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2191
|
+
json: options.json
|
|
2192
|
+
});
|
|
1699
2193
|
process.exit(1);
|
|
1700
2194
|
}
|
|
1701
2195
|
}
|
|
@@ -1714,6 +2208,208 @@ function createShopCommand() {
|
|
|
1714
2208
|
}
|
|
1715
2209
|
}
|
|
1716
2210
|
);
|
|
2211
|
+
products.command("context").description("Get buyer-facing product context").argument("<product-id>", "Product ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (productId, options) => {
|
|
2212
|
+
try {
|
|
2213
|
+
const client = await getClient(options.profile);
|
|
2214
|
+
output(await client.getCommerceProductContext(productId), { json: options.json });
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2217
|
+
json: options.json
|
|
2218
|
+
});
|
|
2219
|
+
process.exit(1);
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
products.command("create-by-shop").description("Create a product by shop ID using a JSON payload").argument("<shop-id>", "Shop ID").requiredOption("--data <json>", "Product JSON payload").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (shopId, options) => {
|
|
2223
|
+
try {
|
|
2224
|
+
const client = await getClient(options.profile);
|
|
2225
|
+
output(await client.createShopProduct(shopId, parseJsonObject(options.data, "--data")), {
|
|
2226
|
+
json: options.json
|
|
2227
|
+
});
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2230
|
+
json: options.json
|
|
2231
|
+
});
|
|
2232
|
+
process.exit(1);
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
products.command("update-by-shop").description("Update a product by shop ID using a JSON payload").argument("<shop-id>", "Shop ID").argument("<product-id>", "Product ID").requiredOption("--data <json>", "Product JSON payload").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2236
|
+
async (shopId, productId, options) => {
|
|
2237
|
+
try {
|
|
2238
|
+
const client = await getClient(options.profile);
|
|
2239
|
+
output(
|
|
2240
|
+
await client.updateShopProduct(
|
|
2241
|
+
shopId,
|
|
2242
|
+
productId,
|
|
2243
|
+
parseJsonObject(options.data, "--data")
|
|
2244
|
+
),
|
|
2245
|
+
{ json: options.json }
|
|
2246
|
+
);
|
|
2247
|
+
} catch (error) {
|
|
2248
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2249
|
+
json: options.json
|
|
2250
|
+
});
|
|
2251
|
+
process.exit(1);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
);
|
|
2255
|
+
products.command("purchase").description("Purchase a product by shop ID").argument("<shop-id>", "Shop ID").argument("<product-id>", "Product ID").option("--sku-id <id>", "SKU ID").requiredOption("--idempotency-key <key>", "Idempotency key").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2256
|
+
async (shopId, productId, options) => {
|
|
2257
|
+
try {
|
|
2258
|
+
const client = await getClient(options.profile);
|
|
2259
|
+
output(
|
|
2260
|
+
await client.purchaseShopProduct(shopId, productId, {
|
|
2261
|
+
skuId: options.skuId,
|
|
2262
|
+
idempotencyKey: options.idempotencyKey
|
|
2263
|
+
}),
|
|
2264
|
+
{ json: options.json }
|
|
2265
|
+
);
|
|
2266
|
+
} catch (error) {
|
|
2267
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2268
|
+
json: options.json
|
|
2269
|
+
});
|
|
2270
|
+
process.exit(1);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
);
|
|
2274
|
+
const offers = shop.command("offers").description("Commerce offer commands");
|
|
2275
|
+
offers.command("list").description("List commerce offers for a shop").argument("<shop-id>", "Shop ID").option("--keyword <keyword>", "Search keyword").option("--limit <n>", "Maximum offers").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2276
|
+
async (shopId, options) => {
|
|
2277
|
+
try {
|
|
2278
|
+
const client = await getClient(options.profile);
|
|
2279
|
+
output(
|
|
2280
|
+
await client.listCommerceOffers(shopId, {
|
|
2281
|
+
keyword: options.keyword,
|
|
2282
|
+
limit: parseOptionalNumber(options.limit, "--limit")
|
|
2283
|
+
}),
|
|
2284
|
+
{ json: options.json }
|
|
2285
|
+
);
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2288
|
+
json: options.json
|
|
2289
|
+
});
|
|
2290
|
+
process.exit(1);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
);
|
|
2294
|
+
offers.command("create").description("Create a commerce offer for a shop").argument("<shop-id>", "Shop ID").requiredOption("--product-id <id>", "Product ID").option("--allowed-surfaces <list>", "Comma-separated surfaces, e.g. channel,dm").option("--price-override <amount>", "Price override").option("--seller-buddy-user-id <id>", "Seller Buddy user ID").option("--status <status>", "Offer status").option("--metadata <json>", "Metadata JSON object").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2295
|
+
async (shopId, options) => {
|
|
2296
|
+
try {
|
|
2297
|
+
const client = await getClient(options.profile);
|
|
2298
|
+
output(
|
|
2299
|
+
await client.createCommerceOffer(shopId, {
|
|
2300
|
+
productId: options.productId,
|
|
2301
|
+
allowedSurfaces: options.allowedSurfaces ? options.allowedSurfaces.split(",").map((item) => item.trim()) : void 0,
|
|
2302
|
+
priceOverride: options.priceOverride ? Number(options.priceOverride) : void 0,
|
|
2303
|
+
sellerBuddyUserId: options.sellerBuddyUserId,
|
|
2304
|
+
status: options.status,
|
|
2305
|
+
metadata: options.metadata ? parseJsonObject(options.metadata, "--metadata") : void 0
|
|
2306
|
+
}),
|
|
2307
|
+
{ json: options.json }
|
|
2308
|
+
);
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2311
|
+
json: options.json
|
|
2312
|
+
});
|
|
2313
|
+
process.exit(1);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
);
|
|
2317
|
+
const deliverables = offers.command("deliverables").description("Commerce deliverable commands");
|
|
2318
|
+
deliverables.command("create").description("Create a deliverable for an offer").argument("<shop-id>", "Shop ID").argument("<offer-id>", "Offer ID").requiredOption("--resource-id <id>", "Resource ID").option("--kind <kind>", "Deliverable kind").option("--resource-type <type>", "Resource type").option("--sender-buddy-user-id <id>", "Sender Buddy user ID").option("--message-template-key <key>", "Message template key").option("--metadata <json>", "Metadata JSON object").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2319
|
+
async (shopId, offerId, options) => {
|
|
2320
|
+
try {
|
|
2321
|
+
const client = await getClient(options.profile);
|
|
2322
|
+
output(
|
|
2323
|
+
await client.createCommerceDeliverable(shopId, offerId, {
|
|
2324
|
+
resourceId: options.resourceId,
|
|
2325
|
+
kind: options.kind,
|
|
2326
|
+
resourceType: options.resourceType,
|
|
2327
|
+
senderBuddyUserId: options.senderBuddyUserId,
|
|
2328
|
+
messageTemplateKey: options.messageTemplateKey,
|
|
2329
|
+
metadata: options.metadata ? parseJsonObject(options.metadata, "--metadata") : void 0
|
|
2330
|
+
}),
|
|
2331
|
+
{ json: options.json }
|
|
2332
|
+
);
|
|
2333
|
+
} catch (error) {
|
|
2334
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2335
|
+
json: options.json
|
|
2336
|
+
});
|
|
2337
|
+
process.exit(1);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
);
|
|
2341
|
+
const assetDefinitions = shop.command("assets").description("Shop asset definition commands");
|
|
2342
|
+
assetDefinitions.command("list").description("List shop asset definitions").argument("<shop-id>", "Shop ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (shopId, options) => {
|
|
2343
|
+
try {
|
|
2344
|
+
const client = await getClient(options.profile);
|
|
2345
|
+
output(await client.listShopAssetDefinitions(shopId), { json: options.json });
|
|
2346
|
+
} catch (error) {
|
|
2347
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2348
|
+
json: options.json
|
|
2349
|
+
});
|
|
2350
|
+
process.exit(1);
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
2353
|
+
assetDefinitions.command("create").description("Create a shop asset definition").argument("<shop-id>", "Shop ID").requiredOption("--asset-type <type>", "Asset type").requiredOption("--name <name>", "Asset name").option("--data <json>", "Additional asset definition JSON").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2354
|
+
async (shopId, options) => {
|
|
2355
|
+
try {
|
|
2356
|
+
const client = await getClient(options.profile);
|
|
2357
|
+
output(
|
|
2358
|
+
await client.createShopAssetDefinition(shopId, {
|
|
2359
|
+
...parseJsonObject(options.data, "--data"),
|
|
2360
|
+
assetType: options.assetType,
|
|
2361
|
+
name: options.name
|
|
2362
|
+
}),
|
|
2363
|
+
{ json: options.json }
|
|
2364
|
+
);
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2367
|
+
json: options.json
|
|
2368
|
+
});
|
|
2369
|
+
process.exit(1);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
);
|
|
2373
|
+
assetDefinitions.command("update").description("Update a shop asset definition").argument("<shop-id>", "Shop ID").argument("<asset-definition-id>", "Asset definition ID").requiredOption("--data <json>", "Asset definition JSON payload").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2374
|
+
async (shopId, assetDefinitionId, options) => {
|
|
2375
|
+
try {
|
|
2376
|
+
const client = await getClient(options.profile);
|
|
2377
|
+
output(
|
|
2378
|
+
await client.updateShopAssetDefinition(
|
|
2379
|
+
shopId,
|
|
2380
|
+
assetDefinitionId,
|
|
2381
|
+
parseJsonObject(options.data, "--data")
|
|
2382
|
+
),
|
|
2383
|
+
{ json: options.json }
|
|
2384
|
+
);
|
|
2385
|
+
} catch (error) {
|
|
2386
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2387
|
+
json: options.json
|
|
2388
|
+
});
|
|
2389
|
+
process.exit(1);
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
);
|
|
2393
|
+
const entitlements = shop.command("entitlements").description("Shop entitlement commands");
|
|
2394
|
+
entitlements.command("list").description("List entitlements for a shop").argument("<shop-id>", "Shop ID").option("--limit <n>", "Maximum entitlements").option("--offset <n>", "Offset").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
2395
|
+
async (shopId, options) => {
|
|
2396
|
+
try {
|
|
2397
|
+
const client = await getClient(options.profile);
|
|
2398
|
+
output(
|
|
2399
|
+
await client.listShopEntitlements(shopId, {
|
|
2400
|
+
limit: parseOptionalNumber(options.limit, "--limit"),
|
|
2401
|
+
offset: options.offset ? Number.parseInt(options.offset, 10) : void 0
|
|
2402
|
+
}),
|
|
2403
|
+
{ json: options.json }
|
|
2404
|
+
);
|
|
2405
|
+
} catch (error) {
|
|
2406
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
2407
|
+
json: options.json
|
|
2408
|
+
});
|
|
2409
|
+
process.exit(1);
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
);
|
|
1717
2413
|
const cart = shop.command("cart").description("Cart commands");
|
|
1718
2414
|
cart.command("list").description("List cart items").argument("<server-id>", "Server ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (serverId, options) => {
|
|
1719
2415
|
try {
|
|
@@ -1765,9 +2461,9 @@ function createShopCommand() {
|
|
|
1765
2461
|
}
|
|
1766
2462
|
|
|
1767
2463
|
// src/commands/status.ts
|
|
1768
|
-
import { Command as
|
|
2464
|
+
import { Command as Command23 } from "commander";
|
|
1769
2465
|
function createStatusCommand() {
|
|
1770
|
-
const status = new
|
|
2466
|
+
const status = new Command23("status").description("Show detailed status information");
|
|
1771
2467
|
status.option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
1772
2468
|
const outputOpts = { json: options.json };
|
|
1773
2469
|
try {
|
|
@@ -1782,7 +2478,7 @@ function createStatusCommand() {
|
|
|
1782
2478
|
}
|
|
1783
2479
|
const client = await getClient(options.profile);
|
|
1784
2480
|
const user = await client.getMe();
|
|
1785
|
-
const
|
|
2481
|
+
const unread = await client.getUnreadCount().catch(() => ({ count: 0 }));
|
|
1786
2482
|
const statusInfo = {
|
|
1787
2483
|
profile: {
|
|
1788
2484
|
name: profileName,
|
|
@@ -1795,7 +2491,7 @@ function createStatusCommand() {
|
|
|
1795
2491
|
avatarUrl: user.avatarUrl
|
|
1796
2492
|
},
|
|
1797
2493
|
stats: {
|
|
1798
|
-
unreadNotifications:
|
|
2494
|
+
unreadNotifications: unread.count
|
|
1799
2495
|
},
|
|
1800
2496
|
connection: {
|
|
1801
2497
|
status: "connected",
|
|
@@ -1845,9 +2541,9 @@ function createStatusCommand() {
|
|
|
1845
2541
|
}
|
|
1846
2542
|
|
|
1847
2543
|
// src/commands/threads.ts
|
|
1848
|
-
import { Command as
|
|
2544
|
+
import { Command as Command24 } from "commander";
|
|
1849
2545
|
function createThreadsCommand() {
|
|
1850
|
-
const threads = new
|
|
2546
|
+
const threads = new Command24("threads").description("Thread commands");
|
|
1851
2547
|
threads.command("list").description("List threads in a channel").argument("<channel-id>", "Channel ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (channelId, options) => {
|
|
1852
2548
|
try {
|
|
1853
2549
|
const client = await getClient(options.profile);
|
|
@@ -1928,52 +2624,1359 @@ function createThreadsCommand() {
|
|
|
1928
2624
|
return threads;
|
|
1929
2625
|
}
|
|
1930
2626
|
|
|
1931
|
-
// src/commands/voice
|
|
1932
|
-
import {
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2627
|
+
// src/commands/voice.ts
|
|
2628
|
+
import { join as join2 } from "path";
|
|
2629
|
+
import { Command as Command25 } from "commander";
|
|
2630
|
+
|
|
2631
|
+
// src/utils/voice-media-bridge.ts
|
|
2632
|
+
import { execFileSync as execFileSync2, spawn } from "child_process";
|
|
2633
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2634
|
+
import { createWriteStream, existsSync } from "fs";
|
|
2635
|
+
import { mkdir, mkdtemp, open, readdir, readFile as readFile2, rm, stat, writeFile as writeFile2 } from "fs/promises";
|
|
2636
|
+
import { createServer } from "http";
|
|
2637
|
+
import { createRequire } from "module";
|
|
2638
|
+
import { homedir, tmpdir } from "os";
|
|
2639
|
+
import { basename as basename2, dirname, join } from "path";
|
|
2640
|
+
var require2 = createRequire(import.meta.url);
|
|
2641
|
+
var DEFAULT_SCREEN_INTERVAL_MS = 1e3;
|
|
2642
|
+
var MAX_POST_BYTES = 20 * 1024 * 1024;
|
|
2643
|
+
var PLAYWRIGHT_VERSION = "1.59.1";
|
|
2644
|
+
var defaultScreenIntervalMs = DEFAULT_SCREEN_INTERVAL_MS;
|
|
2645
|
+
async function runVoiceMediaBridge(options) {
|
|
2646
|
+
const chromeExecutable = await findBrowserExecutable(options.browser, {
|
|
2647
|
+
installBrowser: options.installBrowser,
|
|
2648
|
+
json: options.json
|
|
2649
|
+
});
|
|
2650
|
+
const agoraScriptPath = resolveAgoraBrowserScript(options.agoraSdk);
|
|
2651
|
+
const bridgeClientId = `shadowob-cli-media-bridge-${randomUUID2()}`;
|
|
2652
|
+
const joinResult = await options.client.joinVoiceChannel(options.channelId, {
|
|
2653
|
+
muted: options.muted,
|
|
2654
|
+
clientId: bridgeClientId
|
|
2655
|
+
});
|
|
2656
|
+
const state = createRuntimeState(options, joinResult, agoraScriptPath, bridgeClientId);
|
|
2657
|
+
let chrome = null;
|
|
2658
|
+
try {
|
|
2659
|
+
const server = createServer((req, res) => {
|
|
2660
|
+
void handleBridgeRequest(state, req, res);
|
|
2661
|
+
});
|
|
2662
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
2663
|
+
const address = server.address();
|
|
2664
|
+
if (!address || typeof address === "string") throw new Error("Failed to start media bridge");
|
|
2665
|
+
state.baseUrl = `http://127.0.0.1:${address.port}/${state.token}`;
|
|
2666
|
+
emitBridgeEvent(state, {
|
|
2667
|
+
type: "bridge:started",
|
|
2668
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2669
|
+
detail: {
|
|
2670
|
+
channelId: options.channelId,
|
|
2671
|
+
audioOutDir: options.audioOutDir ?? null,
|
|
2672
|
+
videoOutDir: options.videoOutDir ?? null,
|
|
2673
|
+
screenOutDir: options.screenOutDir ?? null,
|
|
2674
|
+
input: options.stdinPcm ? "stdin-pcm" : options.inputFile ? "file" : null
|
|
2675
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
chrome = await launchChrome(chromeExecutable, `${state.baseUrl}/`, options);
|
|
2678
|
+
attachConsoleInspector(chrome.port, state);
|
|
2679
|
+
if (options.stdinPcm) {
|
|
2680
|
+
startReadingStdinPcm(state);
|
|
2681
|
+
}
|
|
2682
|
+
await waitForBridgeStop(options.durationSeconds, chrome);
|
|
2683
|
+
await shutdownBridge(state, server, chrome, options);
|
|
2684
|
+
} catch (error) {
|
|
2685
|
+
await shutdownBridge(state, null, chrome, options).catch(() => void 0);
|
|
2686
|
+
throw error;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function createRuntimeState(options, joinResult, agoraScriptPath, clientId) {
|
|
2690
|
+
return {
|
|
2691
|
+
options,
|
|
2692
|
+
joinResult,
|
|
2693
|
+
agoraScriptPath,
|
|
2694
|
+
clientId,
|
|
2695
|
+
token: randomUUID2(),
|
|
2696
|
+
baseUrl: "",
|
|
2697
|
+
screenSeq: /* @__PURE__ */ new Map(),
|
|
2698
|
+
audioSinks: /* @__PURE__ */ new Map(),
|
|
2699
|
+
videoSinks: /* @__PURE__ */ new Map(),
|
|
2700
|
+
pcmChunks: [],
|
|
2701
|
+
pcmNextIndex: 0,
|
|
2702
|
+
pendingPcmRequests: /* @__PURE__ */ new Set(),
|
|
2703
|
+
stdinEnded: false,
|
|
2704
|
+
shuttingDown: false
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
async function handleBridgeRequest(state, req, res) {
|
|
2708
|
+
try {
|
|
2709
|
+
const url = new URL(req.url ?? "/", state.baseUrl || "http://127.0.0.1");
|
|
2710
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
2711
|
+
if (parts[0] !== state.token) {
|
|
2712
|
+
sendText(res, 404, "Not found");
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
const route = parts[1] ?? "";
|
|
2716
|
+
if (req.method === "GET" && route === "") {
|
|
2717
|
+
sendHtml(res, bridgeHtml(state.token));
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
if (req.method === "GET" && route === "bridge.js") {
|
|
2721
|
+
sendJavaScript(res, bridgeClientScript());
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
if (req.method === "GET" && route === "agora.js") {
|
|
2725
|
+
sendBuffer(res, await readFile2(state.agoraScriptPath), "application/javascript");
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
if (req.method === "GET" && route === "config") {
|
|
2729
|
+
sendJson(res, {
|
|
2730
|
+
credentials: state.joinResult.credentials,
|
|
1941
2731
|
options: {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2732
|
+
muted: Boolean(state.options.muted),
|
|
2733
|
+
recordAudio: Boolean(state.options.audioOutDir),
|
|
2734
|
+
recordVideo: Boolean(state.options.videoOutDir),
|
|
2735
|
+
recordScreen: Boolean(state.options.screenOutDir),
|
|
2736
|
+
screenIntervalMs: state.options.screenIntervalMs,
|
|
2737
|
+
input: state.options.stdinPcm ? {
|
|
2738
|
+
mode: "stdin-pcm",
|
|
2739
|
+
sampleRate: state.options.stdinSampleRate,
|
|
2740
|
+
channels: state.options.stdinChannels
|
|
2741
|
+
} : state.options.inputFile ? { mode: "file" } : null
|
|
1947
2742
|
}
|
|
1948
2743
|
});
|
|
1949
|
-
|
|
1950
|
-
} catch (error) {
|
|
1951
|
-
outputError(error instanceof Error ? error.message : String(error), {
|
|
1952
|
-
json: options.json
|
|
1953
|
-
});
|
|
1954
|
-
process.exit(1);
|
|
2744
|
+
return;
|
|
1955
2745
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
const client = await getClient(options.profile);
|
|
1960
|
-
const config = await client.getVoiceConfig();
|
|
1961
|
-
output(config, { json: options.json });
|
|
1962
|
-
} catch (error) {
|
|
1963
|
-
outputError(error instanceof Error ? error.message : String(error), {
|
|
1964
|
-
json: options.json
|
|
2746
|
+
if (req.method === "POST" && route === "token") {
|
|
2747
|
+
const result = await state.options.client.renewVoiceCredentials(state.options.channelId, {
|
|
2748
|
+
clientId: state.clientId
|
|
1965
2749
|
});
|
|
1966
|
-
|
|
2750
|
+
state.joinResult = {
|
|
2751
|
+
...state.joinResult,
|
|
2752
|
+
credentials: result.credentials,
|
|
2753
|
+
state: result.state
|
|
2754
|
+
};
|
|
2755
|
+
sendJson(res, result);
|
|
2756
|
+
return;
|
|
1967
2757
|
}
|
|
1968
|
-
|
|
1969
|
-
|
|
2758
|
+
if (req.method === "GET" && route === "input-file") {
|
|
2759
|
+
if (!state.options.inputFile) {
|
|
2760
|
+
sendText(res, 404, "No input file configured");
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
sendBuffer(
|
|
2764
|
+
res,
|
|
2765
|
+
await readFile2(state.options.inputFile),
|
|
2766
|
+
mediaTypeForPath(state.options.inputFile)
|
|
2767
|
+
);
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
if (req.method === "GET" && route === "input-pcm") {
|
|
2771
|
+
handleInputPcmRequest(state, Number(url.searchParams.get("cursor") ?? "0"), res);
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
if (req.method === "POST" && route === "event") {
|
|
2775
|
+
const body = await readRequestBody(req, MAX_POST_BYTES);
|
|
2776
|
+
emitBridgeEvent(state, JSON.parse(body.toString("utf8")));
|
|
2777
|
+
sendJson(res, { ok: true });
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
if (req.method === "POST" && route === "audio") {
|
|
2781
|
+
const uid = sanitizeSegment(url.searchParams.get("uid") ?? "unknown");
|
|
2782
|
+
const sampleRate = Number(url.searchParams.get("sampleRate") ?? "48000");
|
|
2783
|
+
const channels = Number(url.searchParams.get("channels") ?? "1");
|
|
2784
|
+
const body = await readRequestBody(req, MAX_POST_BYTES);
|
|
2785
|
+
await writeAudioChunk(state, uid, body, sampleRate, channels);
|
|
2786
|
+
sendJson(res, { ok: true });
|
|
2787
|
+
return;
|
|
2788
|
+
}
|
|
2789
|
+
if (req.method === "POST" && route === "screen") {
|
|
2790
|
+
const uid = sanitizeSegment(url.searchParams.get("uid") ?? "unknown");
|
|
2791
|
+
const body = await readRequestBody(req, MAX_POST_BYTES);
|
|
2792
|
+
await writeScreenFrame(state, uid, body);
|
|
2793
|
+
sendJson(res, { ok: true });
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
if (req.method === "POST" && route === "video") {
|
|
2797
|
+
const uid = sanitizeSegment(url.searchParams.get("uid") ?? "unknown");
|
|
2798
|
+
const body = await readRequestBody(req, MAX_POST_BYTES);
|
|
2799
|
+
await writeVideoChunk(state, uid, body);
|
|
2800
|
+
sendJson(res, { ok: true });
|
|
2801
|
+
return;
|
|
2802
|
+
}
|
|
2803
|
+
sendText(res, 404, "Not found");
|
|
2804
|
+
} catch (error) {
|
|
2805
|
+
sendJson(res, { ok: false, error: error instanceof Error ? error.message : String(error) }, 500);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
function bridgeHtml(token) {
|
|
2809
|
+
return `<!doctype html>
|
|
2810
|
+
<html>
|
|
2811
|
+
<head>
|
|
2812
|
+
<meta charset="utf-8" />
|
|
2813
|
+
<title>Shadow Voice Bridge</title>
|
|
2814
|
+
<style>
|
|
2815
|
+
body { margin: 0; background: #050607; color: #f3f4f6; font: 14px system-ui, sans-serif; }
|
|
2816
|
+
main { padding: 18px; }
|
|
2817
|
+
video { width: 320px; max-width: 100%; background: #000; }
|
|
2818
|
+
</style>
|
|
2819
|
+
</head>
|
|
2820
|
+
<body>
|
|
2821
|
+
<main>
|
|
2822
|
+
<strong>Shadow Voice Bridge</strong>
|
|
2823
|
+
<div id="status">starting</div>
|
|
2824
|
+
<div id="screens"></div>
|
|
2825
|
+
</main>
|
|
2826
|
+
<script>window.__SHADOW_BRIDGE_TOKEN__ = ${JSON.stringify(token)};</script>
|
|
2827
|
+
<script src="./agora.js"></script>
|
|
2828
|
+
<script type="module" src="./bridge.js"></script>
|
|
2829
|
+
</body>
|
|
2830
|
+
</html>`;
|
|
2831
|
+
}
|
|
2832
|
+
function bridgeClientScript() {
|
|
2833
|
+
return `
|
|
2834
|
+
const token = window.__SHADOW_BRIDGE_TOKEN__;
|
|
2835
|
+
const base = '/' + token;
|
|
2836
|
+
const statusEl = document.getElementById('status');
|
|
2837
|
+
const screenRoot = document.getElementById('screens');
|
|
2838
|
+
const config = await fetch(base + '/config').then((res) => res.json());
|
|
2839
|
+
const AgoraRTC = window.AgoraRTC;
|
|
2840
|
+
AgoraRTC.disableLogUpload?.();
|
|
2841
|
+
AgoraRTC.setLogLevel?.(3);
|
|
2842
|
+
|
|
2843
|
+
const client = AgoraRTC.createClient({ mode: 'rtc', codec: 'vp8' });
|
|
2844
|
+
let credentials = config.credentials;
|
|
2845
|
+
let tokenRenewTimer = null;
|
|
2846
|
+
const audioRecorders = new Map();
|
|
2847
|
+
const screenRecorders = new Map();
|
|
2848
|
+
const videoRecorders = new Map();
|
|
2849
|
+
let inputAudioTrack = null;
|
|
2850
|
+
|
|
2851
|
+
function setStatus(value) {
|
|
2852
|
+
statusEl.textContent = value;
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
async function emit(type, detail = {}) {
|
|
2856
|
+
const payload = { type, timestamp: new Date().toISOString(), ...detail };
|
|
2857
|
+
console.log('[shadow-voice-bridge]', payload);
|
|
2858
|
+
try {
|
|
2859
|
+
await fetch(base + '/event', {
|
|
2860
|
+
method: 'POST',
|
|
2861
|
+
headers: { 'content-type': 'application/json' },
|
|
2862
|
+
body: JSON.stringify(payload),
|
|
2863
|
+
});
|
|
2864
|
+
} catch (error) {
|
|
2865
|
+
console.error('failed to emit bridge event', error);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
function clearTokenRenewal() {
|
|
2870
|
+
if (!tokenRenewTimer) return;
|
|
2871
|
+
clearTimeout(tokenRenewTimer);
|
|
2872
|
+
tokenRenewTimer = null;
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
function scheduleTokenRenewal() {
|
|
2876
|
+
clearTokenRenewal();
|
|
2877
|
+
if (!credentials.expiresAt) return;
|
|
2878
|
+
const renewAt = new Date(credentials.expiresAt).getTime() - 5 * 60_000;
|
|
2879
|
+
const delay = Math.max(30_000, renewAt - Date.now());
|
|
2880
|
+
tokenRenewTimer = setTimeout(() => {
|
|
2881
|
+
void renewTokens();
|
|
2882
|
+
}, delay);
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
async function renewTokens() {
|
|
2886
|
+
const result = await fetch(base + '/token', { method: 'POST' }).then((res) => {
|
|
2887
|
+
if (!res.ok) throw new Error('token renewal failed: ' + res.status);
|
|
2888
|
+
return res.json();
|
|
2889
|
+
});
|
|
2890
|
+
credentials = result.credentials;
|
|
2891
|
+
if (credentials.token) await client.renewToken(credentials.token);
|
|
2892
|
+
scheduleTokenRenewal();
|
|
2893
|
+
void emit('token:renewed', { detail: { expiresAt: credentials.expiresAt } });
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
function int16FromFloat32(input) {
|
|
2897
|
+
const output = new Int16Array(input.length);
|
|
2898
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
2899
|
+
const sample = Math.max(-1, Math.min(1, input[index]));
|
|
2900
|
+
output[index] = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
|
|
2901
|
+
}
|
|
2902
|
+
return output;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
function startAudioRecorder(uid, track) {
|
|
2906
|
+
if (!config.options.recordAudio || audioRecorders.has(String(uid))) return;
|
|
2907
|
+
const mediaTrack = track.getMediaStreamTrack?.();
|
|
2908
|
+
if (!mediaTrack) {
|
|
2909
|
+
void emit('audio:unsupported', { uid: String(uid), message: 'remote audio track has no MediaStreamTrack' });
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
const context = new AudioContext({ sampleRate: 48000 });
|
|
2913
|
+
const source = context.createMediaStreamSource(new MediaStream([mediaTrack]));
|
|
2914
|
+
const processor = context.createScriptProcessor(4096, 1, 1);
|
|
2915
|
+
const mute = context.createGain();
|
|
2916
|
+
mute.gain.value = 0;
|
|
2917
|
+
processor.onaudioprocess = (event) => {
|
|
2918
|
+
const channel = event.inputBuffer.getChannelData(0);
|
|
2919
|
+
const pcm = int16FromFloat32(channel);
|
|
2920
|
+
void fetch(base + '/audio?uid=' + encodeURIComponent(String(uid)) + '&sampleRate=' + context.sampleRate + '&channels=1', {
|
|
2921
|
+
method: 'POST',
|
|
2922
|
+
body: pcm.buffer,
|
|
2923
|
+
}).catch(() => undefined);
|
|
2924
|
+
};
|
|
2925
|
+
source.connect(processor);
|
|
2926
|
+
processor.connect(mute);
|
|
2927
|
+
mute.connect(context.destination);
|
|
2928
|
+
audioRecorders.set(String(uid), { context, source, processor, mute });
|
|
2929
|
+
void emit('audio:recording-started', { uid: String(uid) });
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
function stopAudioRecorder(uid) {
|
|
2933
|
+
const recorder = audioRecorders.get(String(uid));
|
|
2934
|
+
if (!recorder) return;
|
|
2935
|
+
recorder.processor.disconnect();
|
|
2936
|
+
recorder.source.disconnect();
|
|
2937
|
+
recorder.mute.disconnect();
|
|
2938
|
+
void recorder.context.close();
|
|
2939
|
+
audioRecorders.delete(String(uid));
|
|
2940
|
+
void emit('audio:recording-stopped', { uid: String(uid) });
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
async function startScreenRecorder(uid, track) {
|
|
2944
|
+
if ((!config.options.recordScreen && !config.options.recordVideo) || screenRecorders.has(String(uid))) return;
|
|
2945
|
+
const mediaTrack = track.getMediaStreamTrack?.();
|
|
2946
|
+
if (!mediaTrack) {
|
|
2947
|
+
void emit('screen:unsupported', { uid: String(uid), message: 'remote video track has no MediaStreamTrack' });
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
if (config.options.recordVideo && !videoRecorders.has(String(uid))) {
|
|
2951
|
+
if (typeof MediaRecorder === 'undefined') {
|
|
2952
|
+
void emit('video:unsupported', { uid: String(uid), message: 'MediaRecorder is not available' });
|
|
2953
|
+
} else {
|
|
2954
|
+
const stream = new MediaStream([mediaTrack.clone()]);
|
|
2955
|
+
const mimeTypes = [
|
|
2956
|
+
'video/webm;codecs=vp9',
|
|
2957
|
+
'video/webm;codecs=vp8',
|
|
2958
|
+
'video/webm',
|
|
2959
|
+
];
|
|
2960
|
+
const mimeType = mimeTypes.find((candidate) => MediaRecorder.isTypeSupported(candidate)) || '';
|
|
2961
|
+
try {
|
|
2962
|
+
const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined);
|
|
2963
|
+
recorder.ondataavailable = (event) => {
|
|
2964
|
+
if (!event.data || event.data.size === 0) return;
|
|
2965
|
+
void fetch(base + '/video?uid=' + encodeURIComponent(String(uid)), {
|
|
2966
|
+
method: 'POST',
|
|
2967
|
+
body: event.data,
|
|
2968
|
+
}).catch(() => undefined);
|
|
2969
|
+
};
|
|
2970
|
+
recorder.onerror = (event) => {
|
|
2971
|
+
void emit('video:error', { uid: String(uid), message: String(event.error?.message || event.type) });
|
|
2972
|
+
};
|
|
2973
|
+
recorder.start(1000);
|
|
2974
|
+
videoRecorders.set(String(uid), { recorder, stream });
|
|
2975
|
+
void emit('video:recording-started', { uid: String(uid), detail: { mimeType: recorder.mimeType } });
|
|
2976
|
+
} catch (error) {
|
|
2977
|
+
stream.getTracks().forEach((item) => item.stop());
|
|
2978
|
+
void emit('video:unsupported', { uid: String(uid), message: error?.message || String(error) });
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
if (!config.options.recordScreen) return;
|
|
2983
|
+
const video = document.createElement('video');
|
|
2984
|
+
video.muted = true;
|
|
2985
|
+
video.autoplay = true;
|
|
2986
|
+
video.playsInline = true;
|
|
2987
|
+
video.srcObject = new MediaStream([mediaTrack]);
|
|
2988
|
+
screenRoot.append(video);
|
|
2989
|
+
await video.play().catch(() => undefined);
|
|
2990
|
+
const canvas = document.createElement('canvas');
|
|
2991
|
+
const context = canvas.getContext('2d');
|
|
2992
|
+
const timer = setInterval(() => {
|
|
2993
|
+
const width = video.videoWidth || 1280;
|
|
2994
|
+
const height = video.videoHeight || 720;
|
|
2995
|
+
canvas.width = width;
|
|
2996
|
+
canvas.height = height;
|
|
2997
|
+
context.drawImage(video, 0, 0, width, height);
|
|
2998
|
+
canvas.toBlob((blob) => {
|
|
2999
|
+
if (!blob) return;
|
|
3000
|
+
void fetch(base + '/screen?uid=' + encodeURIComponent(String(uid)), {
|
|
3001
|
+
method: 'POST',
|
|
3002
|
+
body: blob,
|
|
3003
|
+
}).catch(() => undefined);
|
|
3004
|
+
}, 'image/png');
|
|
3005
|
+
}, Math.max(250, config.options.screenIntervalMs || 1000));
|
|
3006
|
+
screenRecorders.set(String(uid), { video, timer });
|
|
3007
|
+
void emit('screen:recording-started', { uid: String(uid) });
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
function stopScreenRecorder(uid) {
|
|
3011
|
+
const recorder = screenRecorders.get(String(uid));
|
|
3012
|
+
if (recorder) {
|
|
3013
|
+
clearInterval(recorder.timer);
|
|
3014
|
+
recorder.video.remove();
|
|
3015
|
+
screenRecorders.delete(String(uid));
|
|
3016
|
+
void emit('screen:recording-stopped', { uid: String(uid) });
|
|
3017
|
+
}
|
|
3018
|
+
const videoRecorder = videoRecorders.get(String(uid));
|
|
3019
|
+
if (videoRecorder) {
|
|
3020
|
+
try {
|
|
3021
|
+
if (videoRecorder.recorder.state !== 'inactive') videoRecorder.recorder.stop();
|
|
3022
|
+
} catch {
|
|
3023
|
+
// Best effort shutdown; chunks already emitted are still kept.
|
|
3024
|
+
}
|
|
3025
|
+
videoRecorder.stream.getTracks().forEach((item) => item.stop());
|
|
3026
|
+
videoRecorders.delete(String(uid));
|
|
3027
|
+
void emit('video:recording-stopped', { uid: String(uid) });
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
async function publishInputFile() {
|
|
3032
|
+
const response = await fetch(base + '/input-file');
|
|
3033
|
+
const data = await response.arrayBuffer();
|
|
3034
|
+
const context = new AudioContext();
|
|
3035
|
+
const buffer = await context.decodeAudioData(data.slice(0));
|
|
3036
|
+
await publishAudioBuffer(context, buffer);
|
|
3037
|
+
void emit('input:file-published');
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
async function publishAudioBuffer(context, buffer) {
|
|
3041
|
+
const destination = context.createMediaStreamDestination();
|
|
3042
|
+
inputAudioTrack = AgoraRTC.createCustomAudioTrack({
|
|
3043
|
+
mediaStreamTrack: destination.stream.getAudioTracks()[0],
|
|
3044
|
+
encoderConfig: 'music_standard',
|
|
3045
|
+
});
|
|
3046
|
+
await client.publish([inputAudioTrack]);
|
|
3047
|
+
const source = context.createBufferSource();
|
|
3048
|
+
source.buffer = buffer;
|
|
3049
|
+
source.connect(destination);
|
|
3050
|
+
source.start();
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
async function publishStdinPcm() {
|
|
3054
|
+
const input = config.options.input;
|
|
3055
|
+
const context = new AudioContext({ sampleRate: input.sampleRate });
|
|
3056
|
+
const destination = context.createMediaStreamDestination();
|
|
3057
|
+
inputAudioTrack = AgoraRTC.createCustomAudioTrack({
|
|
3058
|
+
mediaStreamTrack: destination.stream.getAudioTracks()[0],
|
|
3059
|
+
encoderConfig: 'music_standard',
|
|
3060
|
+
});
|
|
3061
|
+
await client.publish([inputAudioTrack]);
|
|
3062
|
+
let cursor = 0;
|
|
3063
|
+
let playAt = context.currentTime + 0.2;
|
|
3064
|
+
void emit('input:stdin-pcm-published', { detail: { sampleRate: input.sampleRate, channels: input.channels } });
|
|
3065
|
+
while (true) {
|
|
3066
|
+
const response = await fetch(base + '/input-pcm?cursor=' + cursor);
|
|
3067
|
+
if (response.status === 204) {
|
|
3068
|
+
if (response.headers.get('x-input-ended') === 'true') break;
|
|
3069
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
3072
|
+
if (!response.ok) throw new Error('input-pcm failed: ' + response.status);
|
|
3073
|
+
cursor = Number(response.headers.get('x-next-cursor') || cursor + 1);
|
|
3074
|
+
const sampleRate = Number(response.headers.get('x-sample-rate') || input.sampleRate);
|
|
3075
|
+
const channels = Number(response.headers.get('x-channels') || input.channels);
|
|
3076
|
+
const pcm = new Int16Array(await response.arrayBuffer());
|
|
3077
|
+
const frames = Math.floor(pcm.length / channels);
|
|
3078
|
+
const buffer = context.createBuffer(1, frames, sampleRate);
|
|
3079
|
+
const channel = buffer.getChannelData(0);
|
|
3080
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
3081
|
+
channel[frame] = pcm[frame * channels] / 32768;
|
|
3082
|
+
}
|
|
3083
|
+
const source = context.createBufferSource();
|
|
3084
|
+
source.buffer = buffer;
|
|
3085
|
+
source.connect(destination);
|
|
3086
|
+
playAt = Math.max(playAt, context.currentTime + 0.05);
|
|
3087
|
+
source.start(playAt);
|
|
3088
|
+
playAt += buffer.duration;
|
|
3089
|
+
}
|
|
3090
|
+
void emit('input:stdin-ended');
|
|
3091
|
+
}
|
|
3092
|
+
|
|
3093
|
+
client.on('user-published', async (user, mediaType) => {
|
|
3094
|
+
await client.subscribe(user, mediaType);
|
|
3095
|
+
if (mediaType === 'audio' && user.audioTrack) {
|
|
3096
|
+
startAudioRecorder(user.uid, user.audioTrack);
|
|
3097
|
+
void emit('remote-audio:subscribed', { uid: String(user.uid) });
|
|
3098
|
+
}
|
|
3099
|
+
if (mediaType === 'video' && user.videoTrack) {
|
|
3100
|
+
await startScreenRecorder(user.uid, user.videoTrack);
|
|
3101
|
+
void emit('remote-screen:subscribed', { uid: String(user.uid) });
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
3104
|
+
|
|
3105
|
+
client.on('token-privilege-will-expire', () => {
|
|
3106
|
+
void renewTokens().catch((error) => emit('token:error', { message: error.message }));
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
client.on('token-privilege-did-expire', () => {
|
|
3110
|
+
void renewTokens().catch((error) => emit('token:error', { message: error.message }));
|
|
3111
|
+
});
|
|
3112
|
+
|
|
3113
|
+
client.on('user-unpublished', (user, mediaType) => {
|
|
3114
|
+
if (mediaType === 'audio') stopAudioRecorder(user.uid);
|
|
3115
|
+
if (mediaType === 'video') stopScreenRecorder(user.uid);
|
|
3116
|
+
void emit('remote-unpublished', { uid: String(user.uid), detail: { mediaType } });
|
|
3117
|
+
});
|
|
3118
|
+
|
|
3119
|
+
window.addEventListener('error', (event) => {
|
|
3120
|
+
void emit('browser:error', { message: event.message, detail: { filename: event.filename, lineno: event.lineno } });
|
|
3121
|
+
});
|
|
3122
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
3123
|
+
void emit('browser:unhandled-rejection', { message: String(event.reason?.message || event.reason) });
|
|
3124
|
+
});
|
|
3125
|
+
|
|
3126
|
+
window.__shadowVoiceBridgeStop = async () => {
|
|
3127
|
+
clearTokenRenewal();
|
|
3128
|
+
for (const uid of [...screenRecorders.keys(), ...videoRecorders.keys()]) {
|
|
3129
|
+
stopScreenRecorder(uid);
|
|
3130
|
+
}
|
|
3131
|
+
if (inputAudioTrack) {
|
|
3132
|
+
inputAudioTrack.stop?.();
|
|
3133
|
+
inputAudioTrack.close?.();
|
|
3134
|
+
inputAudioTrack = null;
|
|
3135
|
+
}
|
|
3136
|
+
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
3137
|
+
await client.leave().catch(() => undefined);
|
|
3138
|
+
void emit('bridge:page-stopped');
|
|
3139
|
+
};
|
|
3140
|
+
|
|
3141
|
+
try {
|
|
3142
|
+
setStatus('joining');
|
|
3143
|
+
await client.join(credentials.appId, credentials.agoraChannelName, credentials.token, credentials.uid);
|
|
3144
|
+
setStatus('joined');
|
|
3145
|
+
void emit('bridge:joined', { detail: { uid: credentials.uid, screenUid: credentials.screenUid } });
|
|
3146
|
+
scheduleTokenRenewal();
|
|
3147
|
+
if (config.options.input?.mode === 'file') await publishInputFile();
|
|
3148
|
+
if (config.options.input?.mode === 'stdin-pcm') void publishStdinPcm().catch((error) => emit('input:error', { message: error.message }));
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
setStatus('error');
|
|
3151
|
+
void emit('bridge:error', { message: error?.message || String(error) });
|
|
3152
|
+
}
|
|
3153
|
+
`;
|
|
3154
|
+
}
|
|
3155
|
+
function resolveAgoraBrowserScript(explicit) {
|
|
3156
|
+
if (explicit) {
|
|
3157
|
+
if (!existsSync(explicit)) throw new Error(`Agora Web SDK script not found: ${explicit}`);
|
|
3158
|
+
return explicit;
|
|
3159
|
+
}
|
|
3160
|
+
if (process.env.SHADOWOB_AGORA_WEB_SDK) {
|
|
3161
|
+
if (!existsSync(process.env.SHADOWOB_AGORA_WEB_SDK)) {
|
|
3162
|
+
throw new Error(`Agora Web SDK script not found: ${process.env.SHADOWOB_AGORA_WEB_SDK}`);
|
|
3163
|
+
}
|
|
3164
|
+
return process.env.SHADOWOB_AGORA_WEB_SDK;
|
|
3165
|
+
}
|
|
3166
|
+
const direct = tryRequireResolve("agora-rtc-sdk-ng/AgoraRTC_N-production.js");
|
|
3167
|
+
if (direct) return direct;
|
|
3168
|
+
const sdkEntry = tryRequireResolve("@shadowob/sdk");
|
|
3169
|
+
if (sdkEntry) {
|
|
3170
|
+
let current = dirname(sdkEntry);
|
|
3171
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
3172
|
+
const candidate = join(
|
|
3173
|
+
current,
|
|
3174
|
+
"node_modules",
|
|
3175
|
+
"agora-rtc-sdk-ng",
|
|
3176
|
+
"AgoraRTC_N-production.js"
|
|
3177
|
+
);
|
|
3178
|
+
if (existsSync(candidate)) return candidate;
|
|
3179
|
+
current = dirname(current);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
throw new Error(
|
|
3183
|
+
"Agora Web SDK is not available. Install agora-rtc-sdk-ng next to the CLI, or pass --agora-sdk / set SHADOWOB_AGORA_WEB_SDK to AgoraRTC_N-production.js."
|
|
3184
|
+
);
|
|
3185
|
+
}
|
|
3186
|
+
function tryRequireResolve(specifier) {
|
|
3187
|
+
try {
|
|
3188
|
+
return require2.resolve(specifier);
|
|
3189
|
+
} catch {
|
|
3190
|
+
return null;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
async function installVoiceTestBrowser(options = {}) {
|
|
3194
|
+
const existing = await resolveVoiceTestBrowserPath();
|
|
3195
|
+
if (existing) return existing;
|
|
3196
|
+
const root = managedBrowserRoot();
|
|
3197
|
+
await mkdir(root, { recursive: true });
|
|
3198
|
+
const executable = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
3199
|
+
const args = ["--yes", `playwright@${PLAYWRIGHT_VERSION}`, "install", "chromium"];
|
|
3200
|
+
const output2 = [];
|
|
3201
|
+
await new Promise((resolve, reject) => {
|
|
3202
|
+
const child = spawn(executable, args, {
|
|
3203
|
+
env: {
|
|
3204
|
+
...process.env,
|
|
3205
|
+
PLAYWRIGHT_BROWSERS_PATH: root
|
|
3206
|
+
},
|
|
3207
|
+
stdio: options.json ? ["ignore", "pipe", "pipe"] : "inherit"
|
|
3208
|
+
});
|
|
3209
|
+
if (options.json) {
|
|
3210
|
+
child.stdout?.on("data", (chunk) => output2.push(Buffer.from(chunk)));
|
|
3211
|
+
child.stderr?.on("data", (chunk) => output2.push(Buffer.from(chunk)));
|
|
3212
|
+
}
|
|
3213
|
+
child.on("error", reject);
|
|
3214
|
+
child.on("exit", (code) => {
|
|
3215
|
+
if (code === 0) resolve();
|
|
3216
|
+
else {
|
|
3217
|
+
const detail = Buffer.concat(output2).toString("utf8").trim();
|
|
3218
|
+
reject(
|
|
3219
|
+
new Error(
|
|
3220
|
+
`Failed to install test Chromium with npx playwright install chromium${detail ? `: ${detail}` : ""}`
|
|
3221
|
+
)
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
});
|
|
3226
|
+
const installed = await resolveVoiceTestBrowserPath();
|
|
3227
|
+
if (!installed)
|
|
3228
|
+
throw new Error(`Chromium was installed under ${root}, but no executable was found`);
|
|
3229
|
+
return installed;
|
|
3230
|
+
}
|
|
3231
|
+
async function resolveVoiceTestBrowserPath() {
|
|
3232
|
+
const root = managedBrowserRoot();
|
|
3233
|
+
if (!existsSync(root)) return null;
|
|
3234
|
+
return findExecutableUnder(root, managedBrowserExecutableNames());
|
|
3235
|
+
}
|
|
3236
|
+
function managedBrowserRoot() {
|
|
3237
|
+
return process.env.SHADOWOB_BROWSER_CACHE_DIR ? join(process.env.SHADOWOB_BROWSER_CACHE_DIR, "playwright") : join(homedir(), ".cache", "shadowob", "browsers", "playwright");
|
|
3238
|
+
}
|
|
3239
|
+
function managedBrowserExecutableNames() {
|
|
3240
|
+
if (process.platform === "darwin") {
|
|
3241
|
+
return ["Chromium.app/Contents/MacOS/Chromium", "Chrome.app/Contents/MacOS/Chrome"];
|
|
3242
|
+
}
|
|
3243
|
+
if (process.platform === "win32") return ["chrome.exe"];
|
|
3244
|
+
return ["chrome", "chromium"];
|
|
3245
|
+
}
|
|
3246
|
+
async function findExecutableUnder(root, names) {
|
|
3247
|
+
const queue = [root];
|
|
3248
|
+
while (queue.length > 0) {
|
|
3249
|
+
const current = queue.shift();
|
|
3250
|
+
let entries;
|
|
3251
|
+
try {
|
|
3252
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
3253
|
+
} catch {
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
for (const entry of entries) {
|
|
3257
|
+
const full = join(current, entry.name);
|
|
3258
|
+
if (entry.isDirectory()) {
|
|
3259
|
+
for (const name of names) {
|
|
3260
|
+
const candidate = join(full, name);
|
|
3261
|
+
if (existsSync(candidate)) return candidate;
|
|
3262
|
+
}
|
|
3263
|
+
queue.push(full);
|
|
3264
|
+
} else if (entry.isFile() && names.includes(entry.name)) {
|
|
3265
|
+
return full;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
return null;
|
|
3270
|
+
}
|
|
3271
|
+
async function findBrowserExecutable(explicit, options = {}) {
|
|
3272
|
+
if (explicit) {
|
|
3273
|
+
if (!existsSync(explicit)) throw new Error(`Browser executable not found: ${explicit}`);
|
|
3274
|
+
return explicit;
|
|
3275
|
+
}
|
|
3276
|
+
if (process.env.SHADOWOB_BROWSER && existsSync(process.env.SHADOWOB_BROWSER)) {
|
|
3277
|
+
return process.env.SHADOWOB_BROWSER;
|
|
3278
|
+
}
|
|
3279
|
+
const managed = await resolveVoiceTestBrowserPath();
|
|
3280
|
+
if (managed) return managed;
|
|
3281
|
+
if (options.installBrowser) {
|
|
3282
|
+
return installVoiceTestBrowser({ json: options.json });
|
|
3283
|
+
}
|
|
3284
|
+
const candidates = process.platform === "darwin" ? [
|
|
3285
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
3286
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
3287
|
+
] : process.platform === "win32" ? [
|
|
3288
|
+
`${process.env.PROGRAMFILES ?? "C:\\Program Files"}\\Google\\Chrome\\Application\\chrome.exe`,
|
|
3289
|
+
`${process.env["PROGRAMFILES(X86)"] ?? "C:\\Program Files (x86)"}\\Google\\Chrome\\Application\\chrome.exe`
|
|
3290
|
+
] : ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"];
|
|
3291
|
+
for (const candidate of candidates) {
|
|
3292
|
+
if (candidate.includes("/") || candidate.includes("\\")) {
|
|
3293
|
+
if (existsSync(candidate)) return candidate;
|
|
3294
|
+
continue;
|
|
3295
|
+
}
|
|
3296
|
+
try {
|
|
3297
|
+
return execFileSync2("which", [candidate], { encoding: "utf8" }).trim();
|
|
3298
|
+
} catch {
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
throw new Error(
|
|
3302
|
+
"No Chrome/Chromium executable found. Run shadowob voice browser install, pass --install-browser, or set SHADOWOB_BROWSER=/path/to/chrome."
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
async function launchChrome(executable, url, options) {
|
|
3306
|
+
const port = await findFreePort();
|
|
3307
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "shadowob-voice-bridge-"));
|
|
3308
|
+
const args = [
|
|
3309
|
+
`--remote-debugging-port=${port}`,
|
|
3310
|
+
"--remote-debugging-address=127.0.0.1",
|
|
3311
|
+
`--user-data-dir=${userDataDir}`,
|
|
3312
|
+
"--no-first-run",
|
|
3313
|
+
"--no-default-browser-check",
|
|
3314
|
+
"--disable-background-networking",
|
|
3315
|
+
"--autoplay-policy=no-user-gesture-required",
|
|
3316
|
+
"--disable-background-timer-throttling",
|
|
3317
|
+
"--disable-renderer-backgrounding",
|
|
3318
|
+
"--disable-sync",
|
|
3319
|
+
"--disable-component-update",
|
|
3320
|
+
"--disable-breakpad",
|
|
3321
|
+
"--disable-gpu",
|
|
3322
|
+
"--password-store=basic",
|
|
3323
|
+
"--use-mock-keychain"
|
|
3324
|
+
];
|
|
3325
|
+
if (!options.headful) args.push("--headless=new");
|
|
3326
|
+
args.push(url);
|
|
3327
|
+
const child = spawn(executable, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3328
|
+
child.port = port;
|
|
3329
|
+
child.userDataDir = userDataDir;
|
|
3330
|
+
let stderrBuffer = "";
|
|
3331
|
+
child.stderr?.on("data", (chunk) => {
|
|
3332
|
+
stderrBuffer += chunk.toString("utf8");
|
|
3333
|
+
const lines = stderrBuffer.split(/\r?\n/);
|
|
3334
|
+
stderrBuffer = lines.pop() ?? "";
|
|
3335
|
+
for (const line of lines) {
|
|
3336
|
+
if (/ERROR|ERR_|Exception/i.test(line) && !isIgnorableChromeStderr(line)) {
|
|
3337
|
+
process.stderr.write(`${line}
|
|
3338
|
+
`);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
});
|
|
3342
|
+
child.on("exit", (code) => {
|
|
3343
|
+
if (code !== 0 && code !== null) {
|
|
3344
|
+
process.stderr.write(`Chrome exited with code ${code}
|
|
3345
|
+
`);
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
await waitForChrome(port);
|
|
3349
|
+
return child;
|
|
3350
|
+
}
|
|
3351
|
+
async function attachConsoleInspector(port, state) {
|
|
3352
|
+
try {
|
|
3353
|
+
const pages = await fetchJson(`http://127.0.0.1:${port}/json/list`);
|
|
3354
|
+
const page = pages.find((item) => item.type === "page" && item.url.startsWith(state.baseUrl));
|
|
3355
|
+
if (!page?.webSocketDebuggerUrl) return;
|
|
3356
|
+
const socket = new WebSocket(page.webSocketDebuggerUrl);
|
|
3357
|
+
const requestUrls = /* @__PURE__ */ new Map();
|
|
3358
|
+
let id = 0;
|
|
3359
|
+
socket.addEventListener("open", () => {
|
|
3360
|
+
for (const method of ["Runtime.enable", "Log.enable", "Network.enable"]) {
|
|
3361
|
+
socket.send(JSON.stringify({ id: ++id, method }));
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
3364
|
+
socket.addEventListener("message", (message) => {
|
|
3365
|
+
const payload = JSON.parse(String(message.data));
|
|
3366
|
+
if (payload.method === "Runtime.exceptionThrown") {
|
|
3367
|
+
emitBridgeEvent(state, {
|
|
3368
|
+
type: "browser:exception",
|
|
3369
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3370
|
+
detail: payload.params
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
if (payload.method === "Log.entryAdded") {
|
|
3374
|
+
const entry = payload.params?.entry;
|
|
3375
|
+
if (entry?.level === "error" && !isIgnorableBrowserDiagnosticUrl(entry.url)) {
|
|
3376
|
+
emitBridgeEvent(state, {
|
|
3377
|
+
type: "browser:console-error",
|
|
3378
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3379
|
+
message: entry.text,
|
|
3380
|
+
detail: { url: entry.url }
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
if (payload.method === "Network.requestWillBeSent") {
|
|
3385
|
+
const params = payload.params;
|
|
3386
|
+
if (params?.requestId && params.request?.url) {
|
|
3387
|
+
requestUrls.set(params.requestId, params.request.url);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
if (payload.method === "Network.loadingFailed") {
|
|
3391
|
+
const params = payload.params;
|
|
3392
|
+
const url = params?.requestId ? requestUrls.get(params.requestId) : void 0;
|
|
3393
|
+
if (params?.errorText && !params.canceled && !isIgnorableBrowserDiagnosticUrl(url)) {
|
|
3394
|
+
emitBridgeEvent(state, {
|
|
3395
|
+
type: "browser:network-failed",
|
|
3396
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3397
|
+
message: params.errorText,
|
|
3398
|
+
detail: url ? { url } : void 0
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
});
|
|
3403
|
+
} catch {
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
function isIgnorableBrowserDiagnosticUrl(url) {
|
|
3407
|
+
if (!url) return false;
|
|
3408
|
+
return url.endsWith("/favicon.ico") || url.includes("statscollector") || url.includes("update.googleapis.com") || url.includes("clients4.google.com");
|
|
3409
|
+
}
|
|
3410
|
+
function isIgnorableChromeStderr(line) {
|
|
3411
|
+
return line.includes("ssl_client_socket_impl.cc") || line.includes("google_apis/gcm") || line.includes("video_capture_service_impl.cc") || line.includes("A BUNDLE group contains a codec collision") || line.includes("Inconsistent congestion control feedback types") || line.includes("task_policy_set TASK_CATEGORY_POLICY") || line.includes("task_policy_set TASK_SUPPRESSION_POLICY");
|
|
3412
|
+
}
|
|
3413
|
+
async function waitForChrome(port) {
|
|
3414
|
+
const deadline = Date.now() + 3e4;
|
|
3415
|
+
while (Date.now() < deadline) {
|
|
3416
|
+
try {
|
|
3417
|
+
await fetchJson(`http://127.0.0.1:${port}/json/version`);
|
|
3418
|
+
return;
|
|
3419
|
+
} catch {
|
|
3420
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
throw new Error("Timed out waiting for Chrome remote debugging");
|
|
3424
|
+
}
|
|
3425
|
+
async function fetchJson(url) {
|
|
3426
|
+
const response = await fetch(url);
|
|
3427
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
3428
|
+
return response.json();
|
|
3429
|
+
}
|
|
3430
|
+
async function findFreePort() {
|
|
3431
|
+
const server = createServer();
|
|
3432
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
3433
|
+
const address = server.address();
|
|
3434
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
3435
|
+
if (!address || typeof address === "string") throw new Error("Failed to allocate port");
|
|
3436
|
+
return address.port;
|
|
3437
|
+
}
|
|
3438
|
+
function startReadingStdinPcm(state) {
|
|
3439
|
+
process.stdin.on("data", (chunk) => {
|
|
3440
|
+
const entry = { index: state.pcmNextIndex++, data: Buffer.from(chunk) };
|
|
3441
|
+
state.pcmChunks.push(entry);
|
|
3442
|
+
if (state.pcmChunks.length > 512) state.pcmChunks.shift();
|
|
3443
|
+
flushPendingPcmRequests(state);
|
|
3444
|
+
});
|
|
3445
|
+
process.stdin.on("end", () => {
|
|
3446
|
+
state.stdinEnded = true;
|
|
3447
|
+
flushPendingPcmRequests(state);
|
|
3448
|
+
});
|
|
3449
|
+
process.stdin.resume();
|
|
3450
|
+
}
|
|
3451
|
+
function handleInputPcmRequest(state, cursor, res) {
|
|
3452
|
+
const next = state.pcmChunks.find((chunk) => chunk.index >= cursor);
|
|
3453
|
+
if (next) {
|
|
3454
|
+
sendPcmChunk(state, res, next);
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
if (state.stdinEnded) {
|
|
3458
|
+
res.writeHead(204, { "x-input-ended": "true" });
|
|
3459
|
+
res.end();
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
const pending = {
|
|
3463
|
+
cursor,
|
|
3464
|
+
res,
|
|
3465
|
+
timer: setTimeout(() => {
|
|
3466
|
+
state.pendingPcmRequests.delete(pending);
|
|
3467
|
+
if (!res.writableEnded) {
|
|
3468
|
+
res.writeHead(204);
|
|
3469
|
+
res.end();
|
|
3470
|
+
}
|
|
3471
|
+
}, 15e3)
|
|
3472
|
+
};
|
|
3473
|
+
state.pendingPcmRequests.add(pending);
|
|
3474
|
+
}
|
|
3475
|
+
function flushPendingPcmRequests(state) {
|
|
3476
|
+
for (const pending of [...state.pendingPcmRequests]) {
|
|
3477
|
+
const next = state.pcmChunks.find((chunk) => chunk.index >= pending.cursor);
|
|
3478
|
+
if (!next && !state.stdinEnded) continue;
|
|
3479
|
+
clearTimeout(pending.timer);
|
|
3480
|
+
state.pendingPcmRequests.delete(pending);
|
|
3481
|
+
if (pending.res.writableEnded) continue;
|
|
3482
|
+
if (next) sendPcmChunk(state, pending.res, next);
|
|
3483
|
+
else {
|
|
3484
|
+
pending.res.writeHead(204, { "x-input-ended": "true" });
|
|
3485
|
+
pending.res.end();
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
function sendPcmChunk(state, res, chunk) {
|
|
3490
|
+
res.writeHead(200, {
|
|
3491
|
+
"content-type": "application/octet-stream",
|
|
3492
|
+
"x-next-cursor": String(chunk.index + 1),
|
|
3493
|
+
"x-sample-rate": String(state.options.stdinSampleRate),
|
|
3494
|
+
"x-channels": String(state.options.stdinChannels)
|
|
3495
|
+
});
|
|
3496
|
+
res.end(chunk.data);
|
|
3497
|
+
}
|
|
3498
|
+
async function writeAudioChunk(state, uid, chunk, sampleRate, channels) {
|
|
3499
|
+
if (!state.options.audioOutDir || chunk.length === 0) return;
|
|
3500
|
+
await mkdir(state.options.audioOutDir, { recursive: true });
|
|
3501
|
+
let sink = state.audioSinks.get(uid);
|
|
3502
|
+
if (!sink) {
|
|
3503
|
+
const path = join(state.options.audioOutDir, `${uid}-${Date.now()}.wav`);
|
|
3504
|
+
const stream = createWriteStream(path);
|
|
3505
|
+
stream.write(wavHeader(sampleRate, channels, 0));
|
|
3506
|
+
sink = { stream, path, sampleRate, channels, bytes: 0 };
|
|
3507
|
+
state.audioSinks.set(uid, sink);
|
|
3508
|
+
emitBridgeEvent(state, {
|
|
3509
|
+
type: "audio:file-started",
|
|
3510
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3511
|
+
uid,
|
|
3512
|
+
path
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
sink.stream.write(chunk);
|
|
3516
|
+
sink.bytes += chunk.length;
|
|
3517
|
+
}
|
|
3518
|
+
async function writeScreenFrame(state, uid, chunk) {
|
|
3519
|
+
if (!state.options.screenOutDir || chunk.length === 0) return;
|
|
3520
|
+
await mkdir(state.options.screenOutDir, { recursive: true });
|
|
3521
|
+
const seq = (state.screenSeq.get(uid) ?? 0) + 1;
|
|
3522
|
+
state.screenSeq.set(uid, seq);
|
|
3523
|
+
const path = join(state.options.screenOutDir, `${uid}-${String(seq).padStart(6, "0")}.png`);
|
|
3524
|
+
await writeFile2(path, chunk);
|
|
3525
|
+
emitBridgeEvent(state, {
|
|
3526
|
+
type: "screen:frame",
|
|
3527
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3528
|
+
uid,
|
|
3529
|
+
path
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
async function writeVideoChunk(state, uid, chunk) {
|
|
3533
|
+
if (!state.options.videoOutDir || chunk.length === 0) return;
|
|
3534
|
+
await mkdir(state.options.videoOutDir, { recursive: true });
|
|
3535
|
+
let sink = state.videoSinks.get(uid);
|
|
3536
|
+
if (!sink) {
|
|
3537
|
+
const path = join(state.options.videoOutDir, `${uid}-${Date.now()}.webm`);
|
|
3538
|
+
const stream = createWriteStream(path);
|
|
3539
|
+
sink = { stream, path, bytes: 0 };
|
|
3540
|
+
state.videoSinks.set(uid, sink);
|
|
3541
|
+
emitBridgeEvent(state, {
|
|
3542
|
+
type: "video:file-started",
|
|
3543
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3544
|
+
uid,
|
|
3545
|
+
path
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
sink.stream.write(chunk);
|
|
3549
|
+
sink.bytes += chunk.length;
|
|
3550
|
+
}
|
|
3551
|
+
async function closeAudioSinks(state) {
|
|
3552
|
+
await Promise.all(
|
|
3553
|
+
[...state.audioSinks.values()].map(
|
|
3554
|
+
(sink) => new Promise((resolve) => {
|
|
3555
|
+
sink.stream.end(async () => {
|
|
3556
|
+
const file = await open(sink.path, "r+");
|
|
3557
|
+
try {
|
|
3558
|
+
await file.write(wavHeader(sink.sampleRate, sink.channels, sink.bytes), 0, 44, 0);
|
|
3559
|
+
} finally {
|
|
3560
|
+
await file.close();
|
|
3561
|
+
}
|
|
3562
|
+
emitBridgeEvent(state, {
|
|
3563
|
+
type: "audio:file-finished",
|
|
3564
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3565
|
+
path: sink.path,
|
|
3566
|
+
detail: { bytes: sink.bytes }
|
|
3567
|
+
});
|
|
3568
|
+
resolve();
|
|
3569
|
+
});
|
|
3570
|
+
})
|
|
3571
|
+
)
|
|
3572
|
+
);
|
|
3573
|
+
state.audioSinks.clear();
|
|
3574
|
+
}
|
|
3575
|
+
async function closeVideoSinks(state) {
|
|
3576
|
+
await Promise.all(
|
|
3577
|
+
[...state.videoSinks.values()].map(
|
|
3578
|
+
(sink) => new Promise((resolve) => {
|
|
3579
|
+
sink.stream.end(() => {
|
|
3580
|
+
emitBridgeEvent(state, {
|
|
3581
|
+
type: "video:file-finished",
|
|
3582
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3583
|
+
path: sink.path,
|
|
3584
|
+
detail: { bytes: sink.bytes }
|
|
3585
|
+
});
|
|
3586
|
+
resolve();
|
|
3587
|
+
});
|
|
3588
|
+
})
|
|
3589
|
+
)
|
|
3590
|
+
);
|
|
3591
|
+
state.videoSinks.clear();
|
|
3592
|
+
}
|
|
3593
|
+
function wavHeader(sampleRate, channels, dataBytes) {
|
|
3594
|
+
const header = Buffer.alloc(44);
|
|
3595
|
+
const byteRate = sampleRate * channels * 2;
|
|
3596
|
+
header.write("RIFF", 0);
|
|
3597
|
+
header.writeUInt32LE(36 + dataBytes, 4);
|
|
3598
|
+
header.write("WAVE", 8);
|
|
3599
|
+
header.write("fmt ", 12);
|
|
3600
|
+
header.writeUInt32LE(16, 16);
|
|
3601
|
+
header.writeUInt16LE(1, 20);
|
|
3602
|
+
header.writeUInt16LE(channels, 22);
|
|
3603
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
3604
|
+
header.writeUInt32LE(byteRate, 28);
|
|
3605
|
+
header.writeUInt16LE(channels * 2, 32);
|
|
3606
|
+
header.writeUInt16LE(16, 34);
|
|
3607
|
+
header.write("data", 36);
|
|
3608
|
+
header.writeUInt32LE(dataBytes, 40);
|
|
3609
|
+
return header;
|
|
3610
|
+
}
|
|
3611
|
+
async function shutdownBridge(state, server, chrome, options) {
|
|
3612
|
+
if (state.shuttingDown) return;
|
|
3613
|
+
state.shuttingDown = true;
|
|
3614
|
+
if (chrome) await stopBridgePage(chrome.port, state).catch(() => void 0);
|
|
3615
|
+
await options.client.leaveVoiceChannel(options.channelId, { clientId: state.clientId }).catch(() => void 0);
|
|
3616
|
+
await closeAudioSinks(state).catch(() => void 0);
|
|
3617
|
+
await closeVideoSinks(state).catch(() => void 0);
|
|
3618
|
+
for (const pending of state.pendingPcmRequests) {
|
|
3619
|
+
clearTimeout(pending.timer);
|
|
3620
|
+
if (!pending.res.writableEnded) {
|
|
3621
|
+
pending.res.writeHead(204, { "x-input-ended": "true" });
|
|
3622
|
+
pending.res.end();
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
state.pendingPcmRequests.clear();
|
|
3626
|
+
if (server) await new Promise((resolve) => server.close(() => resolve()));
|
|
3627
|
+
if (chrome && !options.keepBrowser) {
|
|
3628
|
+
chrome.kill("SIGTERM");
|
|
3629
|
+
if (chrome.userDataDir)
|
|
3630
|
+
await rm(chrome.userDataDir, { recursive: true, force: true }).catch(() => void 0);
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
async function stopBridgePage(port, state) {
|
|
3634
|
+
const pages = await fetchJson(`http://127.0.0.1:${port}/json/list`);
|
|
3635
|
+
const page = pages.find((item) => item.type === "page" && item.url.startsWith(state.baseUrl));
|
|
3636
|
+
if (!page?.webSocketDebuggerUrl) return;
|
|
3637
|
+
const debuggerUrl = page.webSocketDebuggerUrl;
|
|
3638
|
+
await new Promise((resolve) => {
|
|
3639
|
+
const socket = new WebSocket(debuggerUrl);
|
|
3640
|
+
const timer = setTimeout(() => {
|
|
3641
|
+
socket.close();
|
|
3642
|
+
resolve();
|
|
3643
|
+
}, 3e3);
|
|
3644
|
+
const finish = () => {
|
|
3645
|
+
clearTimeout(timer);
|
|
3646
|
+
socket.close();
|
|
3647
|
+
resolve();
|
|
3648
|
+
};
|
|
3649
|
+
socket.addEventListener("open", () => {
|
|
3650
|
+
socket.send(
|
|
3651
|
+
JSON.stringify({
|
|
3652
|
+
id: 1,
|
|
3653
|
+
method: "Runtime.evaluate",
|
|
3654
|
+
params: {
|
|
3655
|
+
expression: "window.__shadowVoiceBridgeStop?.()",
|
|
3656
|
+
awaitPromise: true,
|
|
3657
|
+
returnByValue: true
|
|
3658
|
+
}
|
|
3659
|
+
})
|
|
3660
|
+
);
|
|
3661
|
+
});
|
|
3662
|
+
socket.addEventListener("message", (message) => {
|
|
3663
|
+
const payload = JSON.parse(String(message.data));
|
|
3664
|
+
if (payload.id === 1) finish();
|
|
3665
|
+
});
|
|
3666
|
+
socket.addEventListener("error", finish);
|
|
3667
|
+
socket.addEventListener("close", finish);
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
async function waitForBridgeStop(durationSeconds, chrome) {
|
|
3671
|
+
await new Promise((resolve) => {
|
|
3672
|
+
let timer;
|
|
3673
|
+
const done = () => {
|
|
3674
|
+
if (timer) clearTimeout(timer);
|
|
3675
|
+
process.off("SIGINT", done);
|
|
3676
|
+
process.off("SIGTERM", done);
|
|
3677
|
+
chrome.off("exit", done);
|
|
3678
|
+
resolve();
|
|
3679
|
+
};
|
|
3680
|
+
if (durationSeconds && durationSeconds > 0) {
|
|
3681
|
+
timer = setTimeout(done, durationSeconds * 1e3);
|
|
3682
|
+
}
|
|
3683
|
+
process.once("SIGINT", done);
|
|
3684
|
+
process.once("SIGTERM", done);
|
|
3685
|
+
chrome.once("exit", done);
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
function emitBridgeEvent(state, event) {
|
|
3689
|
+
if (state.options.json) {
|
|
3690
|
+
console.log(JSON.stringify(event));
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
const suffix = event.path ? ` ${event.path}` : event.message ? ` ${event.message}` : "";
|
|
3694
|
+
console.log(`[${event.timestamp}] ${event.type}${event.uid ? ` uid=${event.uid}` : ""}${suffix}`);
|
|
3695
|
+
}
|
|
3696
|
+
function readRequestBody(req, maxBytes) {
|
|
3697
|
+
return new Promise((resolve, reject) => {
|
|
3698
|
+
const chunks = [];
|
|
3699
|
+
let total = 0;
|
|
3700
|
+
req.on("data", (chunk) => {
|
|
3701
|
+
total += chunk.length;
|
|
3702
|
+
if (total > maxBytes) {
|
|
3703
|
+
reject(new Error("Request body is too large"));
|
|
3704
|
+
req.destroy();
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
chunks.push(chunk);
|
|
3708
|
+
});
|
|
3709
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3710
|
+
req.on("error", reject);
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
function sendJson(res, data, status = 200) {
|
|
3714
|
+
sendBuffer(res, Buffer.from(JSON.stringify(data)), "application/json", status);
|
|
3715
|
+
}
|
|
3716
|
+
function sendHtml(res, html) {
|
|
3717
|
+
sendBuffer(res, Buffer.from(html), "text/html; charset=utf-8");
|
|
3718
|
+
}
|
|
3719
|
+
function sendJavaScript(res, js) {
|
|
3720
|
+
sendBuffer(res, Buffer.from(js), "application/javascript; charset=utf-8");
|
|
3721
|
+
}
|
|
3722
|
+
function sendText(res, status, text) {
|
|
3723
|
+
sendBuffer(res, Buffer.from(text), "text/plain; charset=utf-8", status);
|
|
3724
|
+
}
|
|
3725
|
+
function sendBuffer(res, body, contentType, status = 200) {
|
|
3726
|
+
res.writeHead(status, {
|
|
3727
|
+
"content-type": contentType,
|
|
3728
|
+
"content-length": body.length,
|
|
3729
|
+
"cache-control": "no-store"
|
|
3730
|
+
});
|
|
3731
|
+
res.end(body);
|
|
3732
|
+
}
|
|
3733
|
+
function sanitizeSegment(value) {
|
|
3734
|
+
return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80) || "unknown";
|
|
3735
|
+
}
|
|
3736
|
+
function mediaTypeForPath(path) {
|
|
3737
|
+
const name = basename2(path).toLowerCase();
|
|
3738
|
+
if (name.endsWith(".wav")) return "audio/wav";
|
|
3739
|
+
if (name.endsWith(".mp3")) return "audio/mpeg";
|
|
3740
|
+
if (name.endsWith(".ogg")) return "audio/ogg";
|
|
3741
|
+
return "application/octet-stream";
|
|
3742
|
+
}
|
|
3743
|
+
async function validateVoiceBridgeOptions(options) {
|
|
3744
|
+
if (options.inputFile) {
|
|
3745
|
+
const info = await stat(options.inputFile);
|
|
3746
|
+
if (!info.isFile()) throw new Error(`Input audio path is not a file: ${options.inputFile}`);
|
|
3747
|
+
}
|
|
3748
|
+
if (options.audioOutDir) await mkdir(options.audioOutDir, { recursive: true });
|
|
3749
|
+
if (options.videoOutDir) await mkdir(options.videoOutDir, { recursive: true });
|
|
3750
|
+
if (options.screenOutDir) await mkdir(options.screenOutDir, { recursive: true });
|
|
3751
|
+
if (!Number.isFinite(options.stdinSampleRate) || options.stdinSampleRate < 8e3) {
|
|
3752
|
+
throw new Error("--sample-rate must be at least 8000");
|
|
3753
|
+
}
|
|
3754
|
+
if (![1, 2].includes(options.stdinChannels)) {
|
|
3755
|
+
throw new Error("--channels must be 1 or 2");
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
// src/commands/voice.ts
|
|
3760
|
+
function resolveProfileOption(options, command) {
|
|
3761
|
+
return options.profile ?? command.optsWithGlobals().profile;
|
|
3762
|
+
}
|
|
3763
|
+
function createVoiceCommand() {
|
|
3764
|
+
const voice = new Command25("voice").description("Voice channel commands");
|
|
3765
|
+
voice.command("join").description("Join a voice channel and print Agora connection info").argument("<channel-id>", "Voice channel ID").option("--muted", "Join muted").option("--deafened", "Join deafened").option("--watch", "Keep the process attached and print voice events").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
3766
|
+
async (channelId, options, command) => {
|
|
3767
|
+
try {
|
|
3768
|
+
const profile = resolveProfileOption(options, command);
|
|
3769
|
+
const client = await getClient(profile);
|
|
3770
|
+
const clientId = options.watch ? `shadowob-cli-${Date.now()}-${Math.random().toString(36).slice(2)}` : "shadowob-cli";
|
|
3771
|
+
const result = await client.joinVoiceChannel(channelId, {
|
|
3772
|
+
muted: options.muted,
|
|
3773
|
+
deafened: options.deafened,
|
|
3774
|
+
clientId
|
|
3775
|
+
});
|
|
3776
|
+
output(result, { json: options.json });
|
|
3777
|
+
if (!options.watch) return;
|
|
3778
|
+
const socket = await getSocket(profile);
|
|
3779
|
+
socket.on("voice:participant-joined", (event) => output(event, { json: options.json }));
|
|
3780
|
+
socket.on("voice:participant-left", (event) => output(event, { json: options.json }));
|
|
3781
|
+
socket.on("voice:participant-updated", (event) => output(event, { json: options.json }));
|
|
3782
|
+
socket.connect();
|
|
3783
|
+
await socket.waitForConnect();
|
|
3784
|
+
await socket.joinVoiceChannel(channelId, {
|
|
3785
|
+
muted: options.muted,
|
|
3786
|
+
deafened: options.deafened,
|
|
3787
|
+
clientId
|
|
3788
|
+
});
|
|
3789
|
+
process.on("SIGINT", () => {
|
|
3790
|
+
void client.leaveVoiceChannel(channelId, { clientId }).finally(() => process.exit(0));
|
|
3791
|
+
});
|
|
3792
|
+
socket.raw.on("disconnect", () => process.exit(0));
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3795
|
+
json: options.json
|
|
3796
|
+
});
|
|
3797
|
+
process.exit(1);
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
);
|
|
3801
|
+
voice.command("leave").description("Leave a voice channel").argument("<channel-id>", "Voice channel ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
3802
|
+
async (channelId, options, command) => {
|
|
3803
|
+
try {
|
|
3804
|
+
const client = await getClient(resolveProfileOption(options, command));
|
|
3805
|
+
await client.leaveVoiceChannel(channelId);
|
|
3806
|
+
outputSuccess("Left voice channel", { json: options.json });
|
|
3807
|
+
} catch (error) {
|
|
3808
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3809
|
+
json: options.json
|
|
3810
|
+
});
|
|
3811
|
+
process.exit(1);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
);
|
|
3815
|
+
voice.command("status").description("Show voice channel state").argument("<channel-id>", "Voice channel ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
3816
|
+
async (channelId, options, command) => {
|
|
3817
|
+
try {
|
|
3818
|
+
const client = await getClient(resolveProfileOption(options, command));
|
|
3819
|
+
output(await client.getVoiceState(channelId), { json: options.json });
|
|
3820
|
+
} catch (error) {
|
|
3821
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3822
|
+
json: options.json
|
|
3823
|
+
});
|
|
3824
|
+
process.exit(1);
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
);
|
|
3828
|
+
voice.command("mute").description("Set local voice mute state").argument("<channel-id>", "Voice channel ID").option("--off", "Unmute instead of mute").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
|
|
3829
|
+
async (channelId, options, command) => {
|
|
3830
|
+
try {
|
|
3831
|
+
const client = await getClient(resolveProfileOption(options, command));
|
|
3832
|
+
output(await client.updateVoiceState(channelId, { muted: !options.off }), {
|
|
3833
|
+
json: options.json
|
|
3834
|
+
});
|
|
3835
|
+
} catch (error) {
|
|
3836
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3837
|
+
json: options.json
|
|
3838
|
+
});
|
|
3839
|
+
process.exit(1);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
);
|
|
3843
|
+
voice.command("bridge").description(
|
|
3844
|
+
"Join a voice channel through a local Chrome/Chromium media bridge for audio and screen data"
|
|
3845
|
+
).argument("<channel-id>", "Voice channel ID").option("--record-out <dir>", "Record a full media archive with audio WAV and video WebM files").option("--audio-out <dir>", "Record remote audio tracks as per-user WAV files").option("--video-out <dir>", "Record remote video or screen-share tracks as WebM files").option("--screen-out <dir>", "Record remote screen shares as PNG frame sequences").option(
|
|
3846
|
+
"--screen-interval-ms <ms>",
|
|
3847
|
+
"Screen-share capture interval in milliseconds",
|
|
3848
|
+
String(defaultScreenIntervalMs)
|
|
3849
|
+
).option("--input <file>", "Publish an audio file into the voice channel").option("--stdin-pcm", "Publish raw signed 16-bit little-endian PCM from stdin").option("--sample-rate <hz>", "Sample rate for --stdin-pcm", "24000").option("--channels <count>", "Channel count for --stdin-pcm", "1").option("--browser <path>", "Chrome/Chromium executable path, or set SHADOWOB_BROWSER").option("--install-browser", "Install an isolated test Chromium when no managed browser exists").option(
|
|
3850
|
+
"--agora-sdk <path>",
|
|
3851
|
+
"Agora Web SDK browser bundle path, or set SHADOWOB_AGORA_WEB_SDK"
|
|
3852
|
+
).option("--headful", "Run Chrome/Chromium with a visible window").option("--keep-browser", "Keep the browser profile open after the bridge exits").option("--duration <seconds>", "Run for a fixed duration, then leave the voice channel").option("--muted", "Join Shadow voice presence as muted").option("--profile <name>", "Profile to use").option("--json", "Output bridge events as JSON lines").action(
|
|
3853
|
+
async (channelId, options, command) => {
|
|
3854
|
+
try {
|
|
3855
|
+
if (options.input && options.stdinPcm) {
|
|
3856
|
+
throw new Error("Use either --input or --stdin-pcm, not both");
|
|
3857
|
+
}
|
|
3858
|
+
const screenIntervalMs = parsePositiveInt(
|
|
3859
|
+
options.screenIntervalMs,
|
|
3860
|
+
"--screen-interval-ms"
|
|
3861
|
+
);
|
|
3862
|
+
const stdinSampleRate = parsePositiveInt(options.sampleRate, "--sample-rate");
|
|
3863
|
+
const stdinChannels = parsePositiveInt(options.channels, "--channels");
|
|
3864
|
+
const durationSeconds = options.duration ? parsePositiveInt(options.duration, "--duration") : void 0;
|
|
3865
|
+
const audioOutDir = options.audioOut ?? (options.recordOut ? join2(options.recordOut, "audio") : void 0);
|
|
3866
|
+
const videoOutDir = options.videoOut ?? (options.recordOut ? join2(options.recordOut, "video") : void 0);
|
|
3867
|
+
await validateVoiceBridgeOptions({
|
|
3868
|
+
audioOutDir,
|
|
3869
|
+
videoOutDir,
|
|
3870
|
+
screenOutDir: options.screenOut,
|
|
3871
|
+
inputFile: options.input,
|
|
3872
|
+
stdinSampleRate,
|
|
3873
|
+
stdinChannels
|
|
3874
|
+
});
|
|
3875
|
+
const client = await getClient(resolveProfileOption(options, command));
|
|
3876
|
+
await runVoiceMediaBridge({
|
|
3877
|
+
client,
|
|
3878
|
+
channelId,
|
|
3879
|
+
muted: options.muted,
|
|
3880
|
+
browser: options.browser,
|
|
3881
|
+
installBrowser: options.installBrowser,
|
|
3882
|
+
agoraSdk: options.agoraSdk,
|
|
3883
|
+
headful: options.headful,
|
|
3884
|
+
keepBrowser: options.keepBrowser,
|
|
3885
|
+
durationSeconds,
|
|
3886
|
+
audioOutDir,
|
|
3887
|
+
videoOutDir,
|
|
3888
|
+
screenOutDir: options.screenOut,
|
|
3889
|
+
screenIntervalMs,
|
|
3890
|
+
inputFile: options.input,
|
|
3891
|
+
stdinPcm: options.stdinPcm,
|
|
3892
|
+
stdinSampleRate,
|
|
3893
|
+
stdinChannels,
|
|
3894
|
+
json: options.json
|
|
3895
|
+
});
|
|
3896
|
+
} catch (error) {
|
|
3897
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3898
|
+
json: options.json
|
|
3899
|
+
});
|
|
3900
|
+
process.exit(1);
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
);
|
|
3904
|
+
const browser = new Command25("browser").description("Voice bridge browser runtime commands");
|
|
3905
|
+
browser.command("install").description("Install an isolated Chromium runtime for voice bridge tests").option("--json", "Output as JSON").action(async (options) => {
|
|
3906
|
+
try {
|
|
3907
|
+
const executable = await installVoiceTestBrowser({ json: options.json });
|
|
3908
|
+
output({ executable }, { json: options.json });
|
|
3909
|
+
} catch (error) {
|
|
3910
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3911
|
+
json: options.json
|
|
3912
|
+
});
|
|
3913
|
+
process.exit(1);
|
|
3914
|
+
}
|
|
3915
|
+
});
|
|
3916
|
+
browser.command("path").description("Show the installed voice bridge test browser path").option("--json", "Output as JSON").action(async (options) => {
|
|
3917
|
+
try {
|
|
3918
|
+
const executable = await resolveVoiceTestBrowserPath();
|
|
3919
|
+
if (!executable) {
|
|
3920
|
+
throw new Error("No managed voice bridge browser is installed");
|
|
3921
|
+
}
|
|
3922
|
+
output({ executable }, { json: options.json });
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3925
|
+
json: options.json
|
|
3926
|
+
});
|
|
3927
|
+
process.exit(1);
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3930
|
+
voice.addCommand(browser);
|
|
3931
|
+
return voice;
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
// src/commands/voice-enhance.ts
|
|
3935
|
+
import { Command as Command26 } from "commander";
|
|
3936
|
+
function createVoiceEnhanceCommand() {
|
|
3937
|
+
const voice = new Command26("voice-enhance").description("Voice enhancement commands");
|
|
3938
|
+
voice.command("enhance").description("Enhance a voice transcript").requiredOption("--transcript <text>", "Transcript text to enhance").option("--language <lang>", "Language code (e.g. zh-CN, en-US)").option("--no-self-correction", "Disable self-correction").option("--no-list-formatting", "Disable list formatting").option("--no-filler-removal", "Disable filler word removal").option("--tone-adjustment", "Enable tone adjustment").option("--target-tone <tone>", "Target tone (formal, casual, professional)").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
3939
|
+
try {
|
|
3940
|
+
const client = await getClient(options.profile);
|
|
3941
|
+
const result = await client.enhanceVoice({
|
|
3942
|
+
transcript: options.transcript,
|
|
3943
|
+
language: options.language,
|
|
3944
|
+
options: {
|
|
3945
|
+
enableSelfCorrection: options.selfCorrection !== false,
|
|
3946
|
+
enableListFormatting: options.listFormatting !== false,
|
|
3947
|
+
enableFillerRemoval: options.fillerRemoval !== false,
|
|
3948
|
+
enableToneAdjustment: options.toneAdjustment === true,
|
|
3949
|
+
targetTone: options.targetTone
|
|
3950
|
+
}
|
|
3951
|
+
});
|
|
3952
|
+
output(result, { json: options.json });
|
|
3953
|
+
} catch (error) {
|
|
3954
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3955
|
+
json: options.json
|
|
3956
|
+
});
|
|
3957
|
+
process.exit(1);
|
|
3958
|
+
}
|
|
3959
|
+
});
|
|
3960
|
+
voice.command("config").description("Get voice enhancement config").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
|
|
3961
|
+
try {
|
|
3962
|
+
const client = await getClient(options.profile);
|
|
3963
|
+
const config = await client.getVoiceConfig();
|
|
3964
|
+
output(config, { json: options.json });
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
outputError(error instanceof Error ? error.message : String(error), {
|
|
3967
|
+
json: options.json
|
|
3968
|
+
});
|
|
3969
|
+
process.exit(1);
|
|
3970
|
+
}
|
|
3971
|
+
});
|
|
3972
|
+
return voice;
|
|
1970
3973
|
}
|
|
1971
3974
|
|
|
1972
3975
|
// src/commands/workspace.ts
|
|
1973
|
-
import { readFile } from "fs/promises";
|
|
1974
|
-
import { Command as
|
|
3976
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
3977
|
+
import { Command as Command27 } from "commander";
|
|
1975
3978
|
function createWorkspaceCommand() {
|
|
1976
|
-
const workspace = new
|
|
3979
|
+
const workspace = new Command27("workspace").description("Workspace file management commands");
|
|
1977
3980
|
workspace.command("get").description("Get workspace info").argument("<server-id>", "Server ID or slug").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (serverId, options) => {
|
|
1978
3981
|
try {
|
|
1979
3982
|
const client = await getClient(options.profile);
|
|
@@ -2069,7 +4072,7 @@ function createWorkspaceCommand() {
|
|
|
2069
4072
|
async (serverId, options) => {
|
|
2070
4073
|
try {
|
|
2071
4074
|
const client = await getClient(options.profile);
|
|
2072
|
-
const content = await
|
|
4075
|
+
const content = await readFile3(options.file);
|
|
2073
4076
|
const blob = new Blob([content]);
|
|
2074
4077
|
const name = options.name ?? options.file.split("/").pop() ?? "upload";
|
|
2075
4078
|
const result = await client.uploadWorkspaceFile(serverId, blob, name, options.parentId);
|
|
@@ -2155,12 +4158,13 @@ function createWorkspaceCommand() {
|
|
|
2155
4158
|
}
|
|
2156
4159
|
|
|
2157
4160
|
// src/index.ts
|
|
2158
|
-
var program = new
|
|
4161
|
+
var program = new Command28();
|
|
2159
4162
|
program.name("shadowob").description("Shadow CLI \u2014 command-line interface for Shadow servers").version("0.1.0").configureHelp({
|
|
2160
4163
|
sortSubcommands: true
|
|
2161
4164
|
});
|
|
2162
4165
|
program.option("--profile <name>", "Profile to use (default: current)");
|
|
2163
4166
|
program.addCommand(createAuthCommand());
|
|
4167
|
+
program.addCommand(createAppCommand());
|
|
2164
4168
|
program.addCommand(createServersCommand());
|
|
2165
4169
|
program.addCommand(createChannelsCommand());
|
|
2166
4170
|
program.addCommand(createThreadsCommand());
|
|
@@ -2169,6 +4173,7 @@ program.addCommand(createListenCommand());
|
|
|
2169
4173
|
program.addCommand(createDirectMessagesCommand());
|
|
2170
4174
|
program.addCommand(createWorkspaceCommand());
|
|
2171
4175
|
program.addCommand(createShopCommand());
|
|
4176
|
+
program.addCommand(createCommerceCommand());
|
|
2172
4177
|
program.addCommand(createNotificationsCommand());
|
|
2173
4178
|
program.addCommand(createFriendsCommand());
|
|
2174
4179
|
program.addCommand(createInvitesCommand());
|
|
@@ -2183,5 +4188,6 @@ program.addCommand(createCloudCommand());
|
|
|
2183
4188
|
program.addCommand(createApiTokensCommand());
|
|
2184
4189
|
program.addCommand(createDiscoverCommand());
|
|
2185
4190
|
program.addCommand(createProfileCommentsCommand());
|
|
4191
|
+
program.addCommand(createVoiceCommand());
|
|
2186
4192
|
program.addCommand(createVoiceEnhanceCommand());
|
|
2187
4193
|
program.parse();
|