@shadowob/cli 1.1.4 → 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 Command26 } from "commander";
11
+ import { Command as Command28 } from "commander";
10
12
 
11
13
  // src/commands/agents.ts
12
14
  import { Command } from "commander";
@@ -755,10 +757,217 @@ function spawnCloudCli(args) {
755
757
  }
756
758
  }
757
759
 
758
- // src/commands/config.ts
760
+ // src/commands/commerce.ts
761
+ import { randomUUID } from "crypto";
759
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
+
967
+ // src/commands/config.ts
968
+ import { Command as Command8 } from "commander";
760
969
  function createConfigCommand() {
761
- const config = new Command7("config").description("Configuration management commands");
970
+ const config = new Command8("config").description("Configuration management commands");
762
971
  config.command("path").description("Show configuration file path").action(() => {
763
972
  console.log(configManager.getConfigPath());
764
973
  });
@@ -829,9 +1038,9 @@ function createConfigCommand() {
829
1038
  }
830
1039
 
831
1040
  // src/commands/discover.ts
832
- import { Command as Command8 } from "commander";
1041
+ import { Command as Command9 } from "commander";
833
1042
  function createDiscoverCommand() {
834
- const discover = new Command8("discover").description("Discover popular servers and channels");
1043
+ const discover = new Command9("discover").description("Discover popular servers and channels");
835
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) => {
836
1045
  try {
837
1046
  const client = await getClient(options.profile);
@@ -868,9 +1077,9 @@ function createDiscoverCommand() {
868
1077
  }
869
1078
 
870
1079
  // src/commands/dms.ts
871
- import { Command as Command9 } from "commander";
1080
+ import { Command as Command10 } from "commander";
872
1081
  function createDirectMessagesCommand() {
873
- const dms = new Command9("dms").description("Direct message commands");
1082
+ const dms = new Command10("dms").description("Direct message commands");
874
1083
  dms.command("list").description("List direct channels").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
875
1084
  try {
876
1085
  const client = await getClient(options.profile);
@@ -948,9 +1157,9 @@ function createDirectMessagesCommand() {
948
1157
  }
949
1158
 
950
1159
  // src/commands/friends.ts
951
- import { Command as Command10 } from "commander";
1160
+ import { Command as Command11 } from "commander";
952
1161
  function createFriendsCommand() {
953
- const friends = new Command10("friends").description("Friendship management commands");
1162
+ const friends = new Command11("friends").description("Friendship management commands");
954
1163
  friends.command("list").description("List friends").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
955
1164
  try {
956
1165
  const client = await getClient(options.profile);
@@ -1034,9 +1243,9 @@ function createFriendsCommand() {
1034
1243
  }
1035
1244
 
1036
1245
  // src/commands/invites.ts
1037
- import { Command as Command11 } from "commander";
1246
+ import { Command as Command12 } from "commander";
1038
1247
  function createInvitesCommand() {
1039
- const invites = new Command11("invites").description("Invite code management commands");
1248
+ const invites = new Command12("invites").description("Invite code management commands");
1040
1249
  invites.command("list").description("List your invite codes").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
1041
1250
  try {
1042
1251
  const client = await getClient(options.profile);
@@ -1087,15 +1296,15 @@ function createInvitesCommand() {
1087
1296
  }
1088
1297
 
1089
1298
  // src/commands/listen.ts
1090
- import { Command as Command12 } from "commander";
1299
+ import { Command as Command13 } from "commander";
1091
1300
  function createListenCommand() {
1092
- const listen = new Command12("listen").description("Listen to real-time events");
1301
+ const listen = new Command13("listen").description("Listen to real-time events");
1093
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(
1094
1303
  async (channelId, options) => {
1095
1304
  try {
1096
1305
  const eventTypes = options.eventType?.split(",").map((t) => t.trim());
1097
1306
  if (options.mode === "poll") {
1098
- const { getClient: getClient2 } = await import("./client-OCEGJPBJ.js");
1307
+ const { getClient: getClient2 } = await import("./client-ZIUDIQPZ.js");
1099
1308
  const client = await getClient2(options.profile);
1100
1309
  const limit = parseInt(options.last ?? "50", 10);
1101
1310
  const result = await client.getMessages(channelId, limit);
@@ -1209,9 +1418,9 @@ function createListenCommand() {
1209
1418
  }
1210
1419
 
1211
1420
  // src/commands/marketplace.ts
1212
- import { Command as Command13 } from "commander";
1421
+ import { Command as Command14 } from "commander";
1213
1422
  function createMarketplaceCommand() {
1214
- const marketplace = new Command13("marketplace").description("Marketplace commands");
1423
+ const marketplace = new Command14("marketplace").description("Marketplace commands");
1215
1424
  const listings = marketplace.command("listings").description("Listing commands");
1216
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(
1217
1426
  async (options) => {
@@ -1356,9 +1565,9 @@ function createMarketplaceCommand() {
1356
1565
 
1357
1566
  // src/commands/media.ts
1358
1567
  import { readFileSync } from "fs";
1359
- import { Command as Command14 } from "commander";
1568
+ import { Command as Command15 } from "commander";
1360
1569
  function createMediaCommand() {
1361
- const media = new Command14("media").description("Media management commands");
1570
+ const media = new Command15("media").description("Media management commands");
1362
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(
1363
1572
  async (options) => {
1364
1573
  try {
@@ -1424,9 +1633,9 @@ function createMediaCommand() {
1424
1633
  }
1425
1634
 
1426
1635
  // src/commands/notifications.ts
1427
- import { Command as Command15 } from "commander";
1636
+ import { Command as Command16 } from "commander";
1428
1637
  function createNotificationsCommand() {
1429
- const notifications = new Command15("notifications").description("Notification commands");
1638
+ const notifications = new Command16("notifications").description("Notification commands");
1430
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(
1431
1640
  async (options) => {
1432
1641
  try {
@@ -1505,9 +1714,24 @@ function splitIds(value) {
1505
1714
  }
1506
1715
 
1507
1716
  // src/commands/oauth.ts
1508
- import { Command as Command16 } 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
+ }
1509
1733
  function createOAuthCommand() {
1510
- const oauth = new Command16("oauth").description("OAuth management commands");
1734
+ const oauth = new Command17("oauth").description("OAuth management commands");
1511
1735
  oauth.command("list").description("List OAuth apps").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
1512
1736
  try {
1513
1737
  const client = await getClient(options.profile);
@@ -1597,13 +1821,52 @@ function createOAuthCommand() {
1597
1821
  process.exit(1);
1598
1822
  }
1599
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
+ );
1600
1863
  return oauth;
1601
1864
  }
1602
1865
 
1603
1866
  // src/commands/ping.ts
1604
- import { Command as Command17 } from "commander";
1867
+ import { Command as Command18 } from "commander";
1605
1868
  function createPingCommand() {
1606
- const ping = new Command17("ping").description("Test connection to Shadow server");
1869
+ const ping = new Command18("ping").description("Test connection to Shadow server");
1607
1870
  ping.option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
1608
1871
  const startTime = Date.now();
1609
1872
  const outputOpts = { json: options.json };
@@ -1652,9 +1915,9 @@ function createPingCommand() {
1652
1915
  }
1653
1916
 
1654
1917
  // src/commands/profile-comments.ts
1655
- import { Command as Command18 } from "commander";
1918
+ import { Command as Command19 } from "commander";
1656
1919
  function createProfileCommentsCommand() {
1657
- const comments = new Command18("profile-comments").description(
1920
+ const comments = new Command19("profile-comments").description(
1658
1921
  "Profile comment management commands"
1659
1922
  );
1660
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) => {
@@ -1704,9 +1967,9 @@ function createProfileCommentsCommand() {
1704
1967
  }
1705
1968
 
1706
1969
  // src/commands/search.ts
1707
- import { Command as Command19 } from "commander";
1970
+ import { Command as Command20 } from "commander";
1708
1971
  function createSearchCommand() {
1709
- const search = new Command19("search").description("Search commands");
1972
+ const search = new Command20("search").description("Search commands");
1710
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(
1711
1974
  async (options) => {
1712
1975
  try {
@@ -1731,9 +1994,9 @@ function createSearchCommand() {
1731
1994
  }
1732
1995
 
1733
1996
  // src/commands/servers.ts
1734
- import { Command as Command20 } from "commander";
1997
+ import { Command as Command21 } from "commander";
1735
1998
  function createServersCommand() {
1736
- const servers = new Command20("servers").description("Server management commands");
1999
+ const servers = new Command21("servers").description("Server management commands");
1737
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) => {
1738
2001
  try {
1739
2002
  const client = await getClient(options.profile);
@@ -1827,9 +2090,25 @@ function createServersCommand() {
1827
2090
  }
1828
2091
 
1829
2092
  // src/commands/shop.ts
1830
- import { Command as Command21 } 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
+ }
1831
2110
  function createShopCommand() {
1832
- const shop = new Command21("shop").description("Shop commands");
2111
+ const shop = new Command22("shop").description("Shop commands");
1833
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) => {
1834
2113
  try {
1835
2114
  const client = await getClient(options.profile);
@@ -1840,12 +2119,54 @@ function createShopCommand() {
1840
2119
  process.exit(1);
1841
2120
  }
1842
2121
  });
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
+ );
1843
2158
  const products = shop.command("products").description("Product commands");
1844
- 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(
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(
1845
2160
  async (serverId, options) => {
1846
2161
  try {
1847
2162
  const client = await getClient(options.profile);
1848
- const products2 = await client.listProducts(serverId, { categoryId: options.categoryId });
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
+ });
1849
2170
  output(products2, { json: options.json });
1850
2171
  } catch (error) {
1851
2172
  outputError(error instanceof Error ? error.message : String(error), {
@@ -1855,6 +2176,24 @@ function createShopCommand() {
1855
2176
  }
1856
2177
  }
1857
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
+ });
2193
+ process.exit(1);
2194
+ }
2195
+ }
2196
+ );
1858
2197
  products.command("get").description("Get product details").argument("<server-id>", "Server ID").argument("<product-id>", "Product ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
1859
2198
  async (serverId, productId, options) => {
1860
2199
  try {
@@ -1869,34 +2208,42 @@ function createShopCommand() {
1869
2208
  }
1870
2209
  }
1871
2210
  );
1872
- const cart = shop.command("cart").description("Cart commands");
1873
- 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) => {
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) => {
1874
2212
  try {
1875
2213
  const client = await getClient(options.profile);
1876
- const cartData = await client.getCart(serverId);
1877
- output(cartData, { json: options.json });
2214
+ output(await client.getCommerceProductContext(productId), { json: options.json });
1878
2215
  } catch (error) {
1879
- outputError(error instanceof Error ? error.message : String(error), { json: options.json });
2216
+ outputError(error instanceof Error ? error.message : String(error), {
2217
+ json: options.json
2218
+ });
1880
2219
  process.exit(1);
1881
2220
  }
1882
2221
  });
1883
- const orders = shop.command("orders").description("Order commands");
1884
- orders.command("list").description("List orders").argument("<server-id>", "Server ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (serverId, options) => {
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) => {
1885
2223
  try {
1886
2224
  const client = await getClient(options.profile);
1887
- const orders2 = await client.listOrders(serverId);
1888
- output(orders2, { json: options.json });
2225
+ output(await client.createShopProduct(shopId, parseJsonObject(options.data, "--data")), {
2226
+ json: options.json
2227
+ });
1889
2228
  } catch (error) {
1890
- outputError(error instanceof Error ? error.message : String(error), { json: options.json });
2229
+ outputError(error instanceof Error ? error.message : String(error), {
2230
+ json: options.json
2231
+ });
1891
2232
  process.exit(1);
1892
2233
  }
1893
2234
  });
1894
- orders.command("get").description("Get order details").argument("<server-id>", "Server ID").argument("<order-id>", "Order ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
1895
- async (serverId, orderId, options) => {
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) => {
1896
2237
  try {
1897
2238
  const client = await getClient(options.profile);
1898
- const order = await client.getOrder(serverId, orderId);
1899
- output(order, { json: options.json });
2239
+ output(
2240
+ await client.updateShopProduct(
2241
+ shopId,
2242
+ productId,
2243
+ parseJsonObject(options.data, "--data")
2244
+ ),
2245
+ { json: options.json }
2246
+ );
1900
2247
  } catch (error) {
1901
2248
  outputError(error instanceof Error ? error.message : String(error), {
1902
2249
  json: options.json
@@ -1905,49 +2252,243 @@ function createShopCommand() {
1905
2252
  }
1906
2253
  }
1907
2254
  );
1908
- const wallet = shop.command("wallet").description("Wallet commands");
1909
- wallet.command("balance").description("Get wallet balance").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
1910
- try {
1911
- const client = await getClient(options.profile);
1912
- const wallet2 = await client.getWallet();
1913
- output(wallet2, { json: options.json });
1914
- } catch (error) {
1915
- outputError(error instanceof Error ? error.message : String(error), { json: options.json });
1916
- process.exit(1);
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
+ }
1917
2272
  }
1918
- });
1919
- return shop;
1920
- }
1921
-
1922
- // src/commands/status.ts
1923
- import { Command as Command22 } from "commander";
1924
- function createStatusCommand() {
1925
- const status = new Command22("status").description("Show detailed status information");
1926
- status.option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
1927
- const outputOpts = { json: options.json };
1928
- try {
1929
- const profileName = options.profile ?? await configManager.getCurrentProfileName();
1930
- const profile = await configManager.getProfile(options.profile);
1931
- if (!profile) {
1932
- outputError(
1933
- profileName ? `Profile "${profileName}" not found` : "Not authenticated. Run: shadowob auth login",
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
+ }),
1934
2284
  { json: options.json }
1935
2285
  );
2286
+ } catch (error) {
2287
+ outputError(error instanceof Error ? error.message : String(error), {
2288
+ json: options.json
2289
+ });
1936
2290
  process.exit(1);
1937
2291
  }
1938
- const client = await getClient(options.profile);
1939
- const user = await client.getMe();
1940
- const unread = await client.getUnreadCount().catch(() => ({ count: 0 }));
1941
- const statusInfo = {
1942
- profile: {
1943
- name: profileName,
1944
- serverUrl: profile.serverUrl
1945
- },
1946
- user: {
1947
- id: user.id,
1948
- username: user.username,
1949
- displayName: user.displayName,
1950
- avatarUrl: user.avatarUrl
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
+ );
2413
+ const cart = shop.command("cart").description("Cart commands");
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) => {
2415
+ try {
2416
+ const client = await getClient(options.profile);
2417
+ const cartData = await client.getCart(serverId);
2418
+ output(cartData, { json: options.json });
2419
+ } catch (error) {
2420
+ outputError(error instanceof Error ? error.message : String(error), { json: options.json });
2421
+ process.exit(1);
2422
+ }
2423
+ });
2424
+ const orders = shop.command("orders").description("Order commands");
2425
+ orders.command("list").description("List orders").argument("<server-id>", "Server ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (serverId, options) => {
2426
+ try {
2427
+ const client = await getClient(options.profile);
2428
+ const orders2 = await client.listOrders(serverId);
2429
+ output(orders2, { json: options.json });
2430
+ } catch (error) {
2431
+ outputError(error instanceof Error ? error.message : String(error), { json: options.json });
2432
+ process.exit(1);
2433
+ }
2434
+ });
2435
+ orders.command("get").description("Get order details").argument("<server-id>", "Server ID").argument("<order-id>", "Order ID").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(
2436
+ async (serverId, orderId, options) => {
2437
+ try {
2438
+ const client = await getClient(options.profile);
2439
+ const order = await client.getOrder(serverId, orderId);
2440
+ output(order, { json: options.json });
2441
+ } catch (error) {
2442
+ outputError(error instanceof Error ? error.message : String(error), {
2443
+ json: options.json
2444
+ });
2445
+ process.exit(1);
2446
+ }
2447
+ }
2448
+ );
2449
+ const wallet = shop.command("wallet").description("Wallet commands");
2450
+ wallet.command("balance").description("Get wallet balance").option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
2451
+ try {
2452
+ const client = await getClient(options.profile);
2453
+ const wallet2 = await client.getWallet();
2454
+ output(wallet2, { json: options.json });
2455
+ } catch (error) {
2456
+ outputError(error instanceof Error ? error.message : String(error), { json: options.json });
2457
+ process.exit(1);
2458
+ }
2459
+ });
2460
+ return shop;
2461
+ }
2462
+
2463
+ // src/commands/status.ts
2464
+ import { Command as Command23 } from "commander";
2465
+ function createStatusCommand() {
2466
+ const status = new Command23("status").description("Show detailed status information");
2467
+ status.option("--profile <name>", "Profile to use").option("--json", "Output as JSON").action(async (options) => {
2468
+ const outputOpts = { json: options.json };
2469
+ try {
2470
+ const profileName = options.profile ?? await configManager.getCurrentProfileName();
2471
+ const profile = await configManager.getProfile(options.profile);
2472
+ if (!profile) {
2473
+ outputError(
2474
+ profileName ? `Profile "${profileName}" not found` : "Not authenticated. Run: shadowob auth login",
2475
+ { json: options.json }
2476
+ );
2477
+ process.exit(1);
2478
+ }
2479
+ const client = await getClient(options.profile);
2480
+ const user = await client.getMe();
2481
+ const unread = await client.getUnreadCount().catch(() => ({ count: 0 }));
2482
+ const statusInfo = {
2483
+ profile: {
2484
+ name: profileName,
2485
+ serverUrl: profile.serverUrl
2486
+ },
2487
+ user: {
2488
+ id: user.id,
2489
+ username: user.username,
2490
+ displayName: user.displayName,
2491
+ avatarUrl: user.avatarUrl
1951
2492
  },
1952
2493
  stats: {
1953
2494
  unreadNotifications: unread.count
@@ -2000,9 +2541,9 @@ function createStatusCommand() {
2000
2541
  }
2001
2542
 
2002
2543
  // src/commands/threads.ts
2003
- import { Command as Command23 } from "commander";
2544
+ import { Command as Command24 } from "commander";
2004
2545
  function createThreadsCommand() {
2005
- const threads = new Command23("threads").description("Thread commands");
2546
+ const threads = new Command24("threads").description("Thread commands");
2006
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) => {
2007
2548
  try {
2008
2549
  const client = await getClient(options.profile);
@@ -2083,10 +2624,1317 @@ function createThreadsCommand() {
2083
2624
  return threads;
2084
2625
  }
2085
2626
 
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,
2731
+ options: {
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
2742
+ }
2743
+ });
2744
+ return;
2745
+ }
2746
+ if (req.method === "POST" && route === "token") {
2747
+ const result = await state.options.client.renewVoiceCredentials(state.options.channelId, {
2748
+ clientId: state.clientId
2749
+ });
2750
+ state.joinResult = {
2751
+ ...state.joinResult,
2752
+ credentials: result.credentials,
2753
+ state: result.state
2754
+ };
2755
+ sendJson(res, result);
2756
+ return;
2757
+ }
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
+
2086
3934
  // src/commands/voice-enhance.ts
2087
- import { Command as Command24 } from "commander";
3935
+ import { Command as Command26 } from "commander";
2088
3936
  function createVoiceEnhanceCommand() {
2089
- const voice = new Command24("voice-enhance").description("Voice enhancement commands");
3937
+ const voice = new Command26("voice-enhance").description("Voice enhancement commands");
2090
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) => {
2091
3939
  try {
2092
3940
  const client = await getClient(options.profile);
@@ -2125,10 +3973,10 @@ function createVoiceEnhanceCommand() {
2125
3973
  }
2126
3974
 
2127
3975
  // src/commands/workspace.ts
2128
- import { readFile as readFile2 } from "fs/promises";
2129
- import { Command as Command25 } from "commander";
3976
+ import { readFile as readFile3 } from "fs/promises";
3977
+ import { Command as Command27 } from "commander";
2130
3978
  function createWorkspaceCommand() {
2131
- const workspace = new Command25("workspace").description("Workspace file management commands");
3979
+ const workspace = new Command27("workspace").description("Workspace file management commands");
2132
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) => {
2133
3981
  try {
2134
3982
  const client = await getClient(options.profile);
@@ -2224,7 +4072,7 @@ function createWorkspaceCommand() {
2224
4072
  async (serverId, options) => {
2225
4073
  try {
2226
4074
  const client = await getClient(options.profile);
2227
- const content = await readFile2(options.file);
4075
+ const content = await readFile3(options.file);
2228
4076
  const blob = new Blob([content]);
2229
4077
  const name = options.name ?? options.file.split("/").pop() ?? "upload";
2230
4078
  const result = await client.uploadWorkspaceFile(serverId, blob, name, options.parentId);
@@ -2310,7 +4158,7 @@ function createWorkspaceCommand() {
2310
4158
  }
2311
4159
 
2312
4160
  // src/index.ts
2313
- var program = new Command26();
4161
+ var program = new Command28();
2314
4162
  program.name("shadowob").description("Shadow CLI \u2014 command-line interface for Shadow servers").version("0.1.0").configureHelp({
2315
4163
  sortSubcommands: true
2316
4164
  });
@@ -2325,6 +4173,7 @@ program.addCommand(createListenCommand());
2325
4173
  program.addCommand(createDirectMessagesCommand());
2326
4174
  program.addCommand(createWorkspaceCommand());
2327
4175
  program.addCommand(createShopCommand());
4176
+ program.addCommand(createCommerceCommand());
2328
4177
  program.addCommand(createNotificationsCommand());
2329
4178
  program.addCommand(createFriendsCommand());
2330
4179
  program.addCommand(createInvitesCommand());
@@ -2339,5 +4188,6 @@ program.addCommand(createCloudCommand());
2339
4188
  program.addCommand(createApiTokensCommand());
2340
4189
  program.addCommand(createDiscoverCommand());
2341
4190
  program.addCommand(createProfileCommentsCommand());
4191
+ program.addCommand(createVoiceCommand());
2342
4192
  program.addCommand(createVoiceEnhanceCommand());
2343
4193
  program.parse();