@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
- getSocket
6
- } from "./chunk-T3BKMB7N.js";
5
+ getClientWithToken,
6
+ getSocket,
7
+ parsePositiveInt
8
+ } from "./chunk-E364BDQO.js";
7
9
 
8
10
  // src/index.ts
9
- import { Command as Command25 } from "commander";
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 Command3 } from "commander";
430
+ import { Command as Command4 } from "commander";
271
431
  function createAuthCommand() {
272
- const auth = new Command3("auth").description("Authentication commands");
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 Command4 } from "commander";
545
+ import { Command as Command5 } from "commander";
386
546
  function createChannelsCommand() {
387
- const channels = new Command4("channels").description("Channel commands");
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 Command5 } from "commander";
735
+ import { Command as Command6 } from "commander";
576
736
  function createCloudCommand() {
577
- const cloud = new Command5("cloud").description("Shadow Cloud \u2014 deploy AI agent clusters to Kubernetes (via shadowob-cloud)").allowUnknownOption(true).allowExcessArguments(true).action(async (_, cmd) => {
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 Command6 } from "commander";
968
+ import { Command as Command8 } from "commander";
602
969
  function createConfigCommand() {
603
- const config = new Command6("config").description("Configuration management commands");
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 Command7 } from "commander";
1041
+ import { Command as Command9 } from "commander";
675
1042
  function createDiscoverCommand() {
676
- const discover = new Command7("discover").description("Discover popular servers and channels");
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 Command8 } from "commander";
1080
+ import { Command as Command10 } from "commander";
714
1081
  function createDirectMessagesCommand() {
715
- const dms = new Command8("dms").description("Direct message commands");
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 Command9 } from "commander";
1160
+ import { Command as Command11 } from "commander";
794
1161
  function createFriendsCommand() {
795
- const friends = new Command9("friends").description("Friendship management commands");
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 Command10 } from "commander";
1246
+ import { Command as Command12 } from "commander";
880
1247
  function createInvitesCommand() {
881
- const invites = new Command10("invites").description("Invite code management commands");
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 Command11 } from "commander";
1299
+ import { Command as Command13 } from "commander";
933
1300
  function createListenCommand() {
934
- const listen = new Command11("listen").description("Listen to real-time events");
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-OCEGJPBJ.js");
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 Command12 } from "commander";
1421
+ import { Command as Command14 } from "commander";
1055
1422
  function createMarketplaceCommand() {
1056
- const marketplace = new Command12("marketplace").description("Marketplace commands");
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 Command13 } from "commander";
1568
+ import { Command as Command15 } from "commander";
1202
1569
  function createMediaCommand() {
1203
- const media = new Command13("media").description("Media management commands");
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 Command14 } from "commander";
1636
+ import { Command as Command16 } from "commander";
1270
1637
  function createNotificationsCommand() {
1271
- const notifications = new Command14("notifications").description("Notification commands");
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 Command15 } from "commander";
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 Command15("oauth").description("OAuth management commands");
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 Command16 } from "commander";
1867
+ import { Command as Command18 } from "commander";
1411
1868
  function createPingCommand() {
1412
- const ping = new Command16("ping").description("Test connection to Shadow server");
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 Command17 } from "commander";
1918
+ import { Command as Command19 } from "commander";
1462
1919
  function createProfileCommentsCommand() {
1463
- const comments = new Command17("profile-comments").description(
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 Command18 } from "commander";
1970
+ import { Command as Command20 } from "commander";
1514
1971
  function createSearchCommand() {
1515
- const search = new Command18("search").description("Search commands");
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 Command19 } from "commander";
1997
+ import { Command as Command21 } from "commander";
1541
1998
  function createServersCommand() {
1542
- const servers = new Command19("servers").description("Server management commands");
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 Command20 } from "commander";
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 Command20("shop").description("Shop commands");
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
- const products = shop.command("products").description("Product commands");
1689
- products.command("list").description("List products").argument("<server-id>", "Server ID").option("--category-id <id>", "Filter by category").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
1690
- async (serverId, options) => {
1691
- try {
1692
- const client = await getClient(options.profile);
1693
- const products2 = await client.listProducts(serverId, { categoryId: options.categoryId });
1694
- output(products2, { json: options.json });
1695
- } catch (error) {
1696
- outputError(error instanceof Error ? error.message : String(error), {
1697
- json: options.json
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 Command21 } from "commander";
2464
+ import { Command as Command23 } from "commander";
1769
2465
  function createStatusCommand() {
1770
- const status = new Command21("status").description("Show detailed status information");
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 notifications = await client.listNotifications(1).catch(() => []);
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: Array.isArray(notifications) ? notifications.length : 0
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 Command22 } from "commander";
2544
+ import { Command as Command24 } from "commander";
1849
2545
  function createThreadsCommand() {
1850
- const threads = new Command22("threads").description("Thread commands");
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-enhance.ts
1932
- import { Command as Command23 } from "commander";
1933
- function createVoiceEnhanceCommand() {
1934
- const voice = new Command23("voice-enhance").description("Voice enhancement commands");
1935
- 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) => {
1936
- try {
1937
- const client = await getClient(options.profile);
1938
- const result = await client.enhanceVoice({
1939
- transcript: options.transcript,
1940
- language: options.language,
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
- enableSelfCorrection: options.selfCorrection !== false,
1943
- enableListFormatting: options.listFormatting !== false,
1944
- enableFillerRemoval: options.fillerRemoval !== false,
1945
- enableToneAdjustment: options.toneAdjustment === true,
1946
- targetTone: options.targetTone
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
- output(result, { json: options.json });
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
- voice.command("config").description("Get voice enhancement config").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
1958
- try {
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
- process.exit(1);
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
- return voice;
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 Command24 } from "commander";
3976
+ import { readFile as readFile3 } from "fs/promises";
3977
+ import { Command as Command27 } from "commander";
1975
3978
  function createWorkspaceCommand() {
1976
- const workspace = new Command24("workspace").description("Workspace file management commands");
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 readFile(options.file);
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 Command25();
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();