@geoly-ai/social-hub-cli 0.0.12 → 0.0.13

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/admin-index-gates.test.d.ts +2 -0
  3. package/dist/admin-index-gates.test.d.ts.map +1 -0
  4. package/dist/admin-index-gates.test.js +33 -0
  5. package/dist/admin-index-gates.test.js.map +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +684 -63
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.test.js +87 -5
  10. package/dist/index.test.js.map +1 -1
  11. package/dist/permission-runner.d.ts +11 -0
  12. package/dist/permission-runner.d.ts.map +1 -0
  13. package/dist/permission-runner.js +30 -0
  14. package/dist/permission-runner.js.map +1 -0
  15. package/dist/permission-runner.test.d.ts +2 -0
  16. package/dist/permission-runner.test.d.ts.map +1 -0
  17. package/dist/permission-runner.test.js +69 -0
  18. package/dist/permission-runner.test.js.map +1 -0
  19. package/dist/permissions-gates-admin.d.ts +4 -0
  20. package/dist/permissions-gates-admin.d.ts.map +1 -0
  21. package/dist/permissions-gates-admin.js +80 -0
  22. package/dist/permissions-gates-admin.js.map +1 -0
  23. package/dist/permissions-gates-admin.test.d.ts +2 -0
  24. package/dist/permissions-gates-admin.test.d.ts.map +1 -0
  25. package/dist/permissions-gates-admin.test.js +25 -0
  26. package/dist/permissions-gates-admin.test.js.map +1 -0
  27. package/dist/permissions.d.ts.map +1 -1
  28. package/dist/permissions.js +2 -3
  29. package/dist/permissions.js.map +1 -1
  30. package/dist/register-admin.d.ts.map +1 -1
  31. package/dist/register-admin.js +343 -29
  32. package/dist/register-admin.js.map +1 -1
  33. package/dist/register-extensions.d.ts.map +1 -1
  34. package/dist/register-extensions.js +16 -19
  35. package/dist/register-extensions.js.map +1 -1
  36. package/package.json +2 -2
  37. package/skills/README.md +7 -5
  38. package/skills/manifest.json +17 -7
  39. package/skills/social-hub-accounts/SKILL.md +46 -13
  40. package/skills/social-hub-admin/SKILL.md +76 -10
  41. package/skills/social-hub-calendar-jobs/SKILL.md +26 -10
  42. package/skills/social-hub-cli/SKILL.md +35 -191
  43. package/skills/social-hub-cli/evals/evals.json +4 -4
  44. package/skills/social-hub-events-observability/SKILL.md +49 -0
  45. package/skills/social-hub-graph-compliance/SKILL.md +60 -0
  46. package/skills/social-hub-intelligence/SKILL.md +72 -7
  47. package/skills/social-hub-migration/SKILL.md +18 -5
  48. package/skills/social-hub-openclaw-context/SKILL.md +21 -6
  49. package/skills/social-hub-ops-runtime/SKILL.md +10 -5
  50. package/skills/social-hub-posts/SKILL.md +54 -23
  51. package/skills/social-hub-posts/evals/evals.json +23 -0
  52. package/skills/social-hub-publishing/SKILL.md +75 -11
  53. package/skills/social-hub-shared/SKILL.md +8 -4
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ import { getSocialHubHealth } from "@geoly-ai/social-hub-sdk";
4
4
  import { getBaseUrl, requireClient } from "./client.js";
5
5
  import { registerAccountsExtensions, registerAuthAndConfig, registerPermissionsExplain, registerRedditExtensions, } from "./register-extensions.js";
6
6
  import { registerAdminCommands } from "./register-admin.js";
7
+ import { assertApplyOrDryRun } from "./dry-run.js";
8
+ import { withPermissionGate } from "./permission-runner.js";
7
9
  import { registerBatchAndExport } from "./register-batch.js";
8
10
  import { registerAgentContext, registerOpsRuntime, } from "./register-ops.js";
9
11
  import { registerDoctorAndVersion } from "./register-shared.js";
@@ -604,6 +606,28 @@ compliance
604
606
  const res = await requireClient().upsertAccountRiskProfile(opts.team, opts.account, body);
605
607
  console.log(JSON.stringify(res, null, 2));
606
608
  });
609
+ compliance
610
+ .command("rule-caches-list")
611
+ .description("GET /subreddit-rule-caches")
612
+ .requiredOption("-t, --team <teamId>", "Team UUID")
613
+ .option("--include-expired", "include expired caches")
614
+ .action(async (opts) => {
615
+ const res = await requireClient().listSubredditRuleCaches(opts.team, {
616
+ includeExpired: Boolean(opts.includeExpired),
617
+ });
618
+ console.log(JSON.stringify(res, null, 2));
619
+ });
620
+ compliance
621
+ .command("rule-caches-upsert")
622
+ .description("PUT /subreddit-rule-caches/:subreddit")
623
+ .requiredOption("-t, --team <teamId>", "Team UUID")
624
+ .requiredOption("--sub <name>", "subreddit name")
625
+ .requiredOption("-j, --json <json>", "{ rulesMarkdown, sourceUrl?, expiresInDays? }")
626
+ .action(async (opts) => {
627
+ const body = JSON.parse(opts.json);
628
+ const res = await requireClient().upsertSubredditRuleCache(opts.team, opts.sub, body);
629
+ console.log(JSON.stringify(res, null, 2));
630
+ });
607
631
  // --- openclaw ---
608
632
  program
609
633
  .command("openclaw-ingest")
@@ -716,8 +740,14 @@ agentTeams
716
740
  .command("list")
717
741
  .description("GET /v1/agent-teams")
718
742
  .action(async () => {
719
- const res = await requireClient().listAgentTeams();
720
- console.log(JSON.stringify(res, null, 2));
743
+ await withPermissionGate({
744
+ command: "agent-teams list",
745
+ resource: "agentTeam",
746
+ action: "list",
747
+ }, async () => {
748
+ const res = await requireClient().listAgentTeams();
749
+ console.log(JSON.stringify(res, null, 2));
750
+ });
721
751
  });
722
752
  agentTeams
723
753
  .command("workspace-docs")
@@ -726,30 +756,71 @@ agentTeams
726
756
  .option("--kind <kind>", "file|daily_report|weekly_report|ops_record")
727
757
  .option("-n, --limit <n>", "limit", "50")
728
758
  .action(async (opts) => {
729
- const res = await requireClient().listWorkspaceDocuments(opts.team, {
730
- kind: opts.kind,
731
- limit: Number(opts.limit),
759
+ await withPermissionGate({
760
+ command: "agent-teams workspace-docs",
761
+ resource: "agentTeam",
762
+ action: "read",
763
+ teamId: opts.team,
764
+ }, async () => {
765
+ const res = await requireClient().listWorkspaceDocuments(opts.team, {
766
+ kind: opts.kind,
767
+ limit: Number(opts.limit),
768
+ });
769
+ console.log(JSON.stringify(res, null, 2));
732
770
  });
733
- console.log(JSON.stringify(res, null, 2));
734
771
  });
735
772
  agentTeams
736
773
  .command("create")
737
774
  .description("POST /v1/agent-teams — 新建团队(admin only)")
738
775
  .requiredOption("--slug <slug>", "团队 slug(小写字母、数字、横线)")
739
776
  .requiredOption("--name <name>", "团队名称")
740
- .action(async (opts) => {
741
- const res = await requireClient().createTeam({ slug: opts.slug, name: opts.name });
742
- console.log(JSON.stringify(res, null, 2));
777
+ .option("--dry-run", "预览", false)
778
+ .option("--apply", "执行写入", false)
779
+ .action(async (opts) => {
780
+ await withPermissionGate({
781
+ command: "agent-teams create",
782
+ resource: "agentTeam",
783
+ action: "create",
784
+ }, async () => {
785
+ if (!assertApplyOrDryRun(opts, {
786
+ command: "agent-teams create",
787
+ danger: "admin",
788
+ summary: { slug: opts.slug, name: opts.name },
789
+ })) {
790
+ return;
791
+ }
792
+ const res = await requireClient().createTeam({
793
+ slug: opts.slug,
794
+ name: opts.name,
795
+ });
796
+ console.log(JSON.stringify(res, null, 2));
797
+ });
743
798
  });
744
799
  agentTeams
745
800
  .command("update")
746
801
  .description("PATCH /v1/agent-teams/:teamId — 更新团队(admin only)")
747
802
  .requiredOption("-t, --team <teamId>", "Team UUID")
748
803
  .requiredOption("-j, --json <json>", "{ slug?, name? }")
749
- .action(async (opts) => {
750
- const body = JSON.parse(opts.json);
751
- const res = await requireClient().updateTeam(opts.team, body);
752
- console.log(JSON.stringify(res, null, 2));
804
+ .option("--dry-run", "预览", false)
805
+ .option("--apply", "执行写入", false)
806
+ .action(async (opts) => {
807
+ await withPermissionGate({
808
+ command: "agent-teams update",
809
+ resource: "agentTeam",
810
+ action: "update",
811
+ teamId: opts.team,
812
+ }, async () => {
813
+ const body = JSON.parse(opts.json);
814
+ if (!assertApplyOrDryRun(opts, {
815
+ command: "agent-teams update",
816
+ danger: "admin",
817
+ summary: { teamId: opts.team, ...body },
818
+ })) {
819
+ return;
820
+ }
821
+ const res = await requireClient().updateTeam(opts.team, body);
822
+ console.log(JSON.stringify(res, null, 2));
823
+ });
753
824
  });
754
825
  agentTeams
755
826
  .command("add-member")
@@ -757,12 +828,32 @@ agentTeams
757
828
  .requiredOption("-t, --team <teamId>", "Team UUID")
758
829
  .requiredOption("--user <userId>", "User UUID")
759
830
  .requiredOption("--role <role>", "admin|manager|supervisor|senior|internal|client")
760
- .action(async (opts) => {
761
- const res = await requireClient().addTeamMember(opts.team, {
762
- userId: opts.user,
763
- role: opts.role,
831
+ .option("--dry-run", "预览", false)
832
+ .option("--apply", "执行写入", false)
833
+ .action(async (opts) => {
834
+ await withPermissionGate({
835
+ command: "agent-teams add-member",
836
+ resource: "agentTeam",
837
+ action: "update",
838
+ teamId: opts.team,
839
+ }, async () => {
840
+ if (!assertApplyOrDryRun(opts, {
841
+ command: "agent-teams add-member",
842
+ danger: "admin",
843
+ summary: {
844
+ teamId: opts.team,
845
+ userId: opts.user,
846
+ role: opts.role,
847
+ },
848
+ })) {
849
+ return;
850
+ }
851
+ const res = await requireClient().addTeamMember(opts.team, {
852
+ userId: opts.user,
853
+ role: opts.role,
854
+ });
855
+ console.log(JSON.stringify(res, null, 2));
764
856
  });
765
- console.log(JSON.stringify(res, null, 2));
766
857
  });
767
858
  // --- intelligence (subreddit-intelligence) ---
768
859
  const intel = program.command("intelligence").description("板块情报与 KOL 挖掘");
@@ -903,6 +994,192 @@ intel
903
994
  const res = await requireClient().dispatchTaskFromHotPost(opts.team, body);
904
995
  console.log(JSON.stringify(res, null, 2));
905
996
  });
997
+ intel
998
+ .command("tier-rules-list")
999
+ .description("GET .../subreddit-intelligence/tier-rules")
1000
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1001
+ .option("--tier <tier>", "tier filter")
1002
+ .option("--enabled <bool>", "true|false")
1003
+ .option("-n, --limit <n>", "limit", "50")
1004
+ .option("--offset <n>", "offset", "0")
1005
+ .action(async (opts) => {
1006
+ const res = await requireClient().listTierRules(opts.team, {
1007
+ tier: opts.tier,
1008
+ enabled: opts.enabled === undefined
1009
+ ? undefined
1010
+ : opts.enabled === "true" || opts.enabled === "1",
1011
+ limit: Number(opts.limit),
1012
+ offset: Number(opts.offset),
1013
+ });
1014
+ console.log(JSON.stringify(res, null, 2));
1015
+ });
1016
+ intel
1017
+ .command("tier-rules-create")
1018
+ .description("POST .../subreddit-intelligence/tier-rules")
1019
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1020
+ .requiredOption("-j, --json <json>", "tier rule body")
1021
+ .action(async (opts) => {
1022
+ const body = JSON.parse(opts.json);
1023
+ const res = await requireClient().createTierRule(opts.team, body);
1024
+ console.log(JSON.stringify(res, null, 2));
1025
+ });
1026
+ intel
1027
+ .command("tier-rules-update")
1028
+ .description("PATCH .../subreddit-intelligence/tier-rules/:id")
1029
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1030
+ .requiredOption("--id <uuid>", "Tier rule UUID")
1031
+ .requiredOption("-j, --json <json>", "update body")
1032
+ .action(async (opts) => {
1033
+ const body = JSON.parse(opts.json);
1034
+ const res = await requireClient().updateTierRule(opts.team, opts.id, body);
1035
+ console.log(JSON.stringify(res, null, 2));
1036
+ });
1037
+ intel
1038
+ .command("tier-rules-delete")
1039
+ .description("DELETE .../subreddit-intelligence/tier-rules/:id")
1040
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1041
+ .requiredOption("--id <uuid>", "Tier rule UUID")
1042
+ .action(async (opts) => {
1043
+ const res = await requireClient().deleteTierRule(opts.team, opts.id);
1044
+ console.log(JSON.stringify(res, null, 2));
1045
+ });
1046
+ intel
1047
+ .command("runs-list")
1048
+ .description("GET .../subreddit-intelligence/runs")
1049
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1050
+ .option("--dimension <d>", "industry|keywords|tier")
1051
+ .option("--status <s>", "queued|running|succeeded|failed")
1052
+ .option("-n, --limit <n>", "limit", "50")
1053
+ .option("--offset <n>", "offset", "0")
1054
+ .action(async (opts) => {
1055
+ const res = await requireClient().listIntelRuns(opts.team, {
1056
+ dimension: opts.dimension,
1057
+ status: opts.status,
1058
+ limit: Number(opts.limit),
1059
+ offset: Number(opts.offset),
1060
+ });
1061
+ console.log(JSON.stringify(res, null, 2));
1062
+ });
1063
+ intel
1064
+ .command("kol-profiles-list")
1065
+ .description("GET .../subreddit-intelligence/kol-profiles")
1066
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1067
+ .option("--platform <p>", "reddit")
1068
+ .option("-n, --limit <n>", "limit", "50")
1069
+ .option("--offset <n>", "offset", "0")
1070
+ .action(async (opts) => {
1071
+ const res = await requireClient().listKolProfiles(opts.team, {
1072
+ platform: opts.platform,
1073
+ limit: Number(opts.limit),
1074
+ offset: Number(opts.offset),
1075
+ });
1076
+ console.log(JSON.stringify(res, null, 2));
1077
+ });
1078
+ intel
1079
+ .command("kol-profiles-get")
1080
+ .description("GET .../subreddit-intelligence/kol-profiles/:id")
1081
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1082
+ .requiredOption("--id <uuid>", "KOL profile UUID")
1083
+ .action(async (opts) => {
1084
+ const res = await requireClient().getKolProfile(opts.team, opts.id);
1085
+ console.log(JSON.stringify(res, null, 2));
1086
+ });
1087
+ intel
1088
+ .command("fetch-by-industry")
1089
+ .description("POST .../subreddit-intelligence/hot-posts/fetch-by-industry")
1090
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1091
+ .requiredOption("-j, --json <json>", "fetch body")
1092
+ .action(async (opts) => {
1093
+ const body = JSON.parse(opts.json);
1094
+ const res = await requireClient().fetchHotPostsByIndustry(opts.team, body);
1095
+ console.log(JSON.stringify(res, null, 2));
1096
+ });
1097
+ intel
1098
+ .command("fetch-by-tier")
1099
+ .description("POST .../subreddit-intelligence/hot-posts/fetch-by-tier")
1100
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1101
+ .requiredOption("-j, --json <json>", "fetch body")
1102
+ .action(async (opts) => {
1103
+ const body = JSON.parse(opts.json);
1104
+ const res = await requireClient().fetchHotPostsByTier(opts.team, body);
1105
+ console.log(JSON.stringify(res, null, 2));
1106
+ });
1107
+ intel
1108
+ .command("fetch-by-keywords")
1109
+ .description("POST .../subreddit-intelligence/insights/fetch-by-keywords")
1110
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1111
+ .requiredOption("-j, --json <json>", "fetch body")
1112
+ .action(async (opts) => {
1113
+ const body = JSON.parse(opts.json);
1114
+ const res = await requireClient().fetchInsightsByKeywords(opts.team, body);
1115
+ console.log(JSON.stringify(res, null, 2));
1116
+ });
1117
+ intel
1118
+ .command("brand-mention-radar")
1119
+ .description("POST .../subreddit-intelligence/insights/brand-mention-radar")
1120
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1121
+ .requiredOption("-j, --json <json>", "{ keywords, minScore?, minComments? }")
1122
+ .action(async (opts) => {
1123
+ const body = JSON.parse(opts.json);
1124
+ const res = await requireClient().getBrandMentionRadar(opts.team, body);
1125
+ console.log(JSON.stringify(res, null, 2));
1126
+ });
1127
+ intel
1128
+ .command("opportunity-map")
1129
+ .description("POST .../subreddit-intelligence/insights/opportunity-map")
1130
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1131
+ .requiredOption("-j, --json <json>", "{ keywords, minScore?, minComments? }")
1132
+ .action(async (opts) => {
1133
+ const body = JSON.parse(opts.json);
1134
+ const res = await requireClient().getOpportunityMap(opts.team, body);
1135
+ console.log(JSON.stringify(res, null, 2));
1136
+ });
1137
+ intel
1138
+ .command("sov")
1139
+ .description("POST .../subreddit-intelligence/insights/sov")
1140
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1141
+ .requiredOption("-j, --json <json>", "{ brandKeywords, competitorKeywords, minScore?, minComments? }")
1142
+ .action(async (opts) => {
1143
+ const body = JSON.parse(opts.json);
1144
+ const res = await requireClient().getInsightsSov(opts.team, body);
1145
+ console.log(JSON.stringify(res, null, 2));
1146
+ });
1147
+ intel
1148
+ .command("sentiment-intent")
1149
+ .description("GET .../subreddit-intelligence/insights/sentiment-intent")
1150
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1151
+ .action(async (opts) => {
1152
+ const res = await requireClient().getSentimentIntent(opts.team);
1153
+ console.log(JSON.stringify(res, null, 2));
1154
+ });
1155
+ intel
1156
+ .command("campaign-lift")
1157
+ .description("POST .../subreddit-intelligence/insights/campaign-lift")
1158
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1159
+ .requiredOption("-j, --json <json>", "{ keywords, campaignStartAt, campaignEndAt, windowDays?, minScore?, minComments? }")
1160
+ .action(async (opts) => {
1161
+ const body = JSON.parse(opts.json);
1162
+ const res = await requireClient().getCampaignLift(opts.team, body);
1163
+ console.log(JSON.stringify(res, null, 2));
1164
+ });
1165
+ intel
1166
+ .command("snapshot-latest")
1167
+ .description("GET .../subreddit-intelligence/insights/snapshot-latest")
1168
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1169
+ .action(async (opts) => {
1170
+ const res = await requireClient().getLatestInsightSnapshot(opts.team);
1171
+ console.log(JSON.stringify(res, null, 2));
1172
+ });
1173
+ intel
1174
+ .command("snapshot-save")
1175
+ .description("POST .../subreddit-intelligence/insights/snapshot")
1176
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1177
+ .requiredOption("-j, --json <json>", "{ payload: { ... } }")
1178
+ .action(async (opts) => {
1179
+ const body = JSON.parse(opts.json);
1180
+ const res = await requireClient().saveInsightSnapshot(opts.team, body);
1181
+ console.log(JSON.stringify(res, null, 2));
1182
+ });
906
1183
  // --- users ---
907
1184
  const usersCmd = program.command("users").description("团队成员管理");
908
1185
  usersCmd
@@ -910,8 +1187,28 @@ usersCmd
910
1187
  .description("GET /users")
911
1188
  .requiredOption("-t, --team <teamId>", "Team UUID")
912
1189
  .action(async (opts) => {
913
- const res = await requireClient().listUsers(opts.team);
914
- console.log(JSON.stringify(res, null, 2));
1190
+ await withPermissionGate({
1191
+ command: "users list",
1192
+ resource: "systemMember",
1193
+ action: "list",
1194
+ teamId: opts.team,
1195
+ }, async () => {
1196
+ const res = await requireClient().listUsers(opts.team);
1197
+ console.log(JSON.stringify(res, null, 2));
1198
+ });
1199
+ });
1200
+ usersCmd
1201
+ .command("system-list")
1202
+ .description("GET /v1/system/users")
1203
+ .action(async () => {
1204
+ await withPermissionGate({
1205
+ command: "users system-list",
1206
+ resource: "systemMember",
1207
+ action: "list",
1208
+ }, async () => {
1209
+ const res = await requireClient().listSystemUsers();
1210
+ console.log(JSON.stringify(res, null, 2));
1211
+ });
915
1212
  });
916
1213
  usersCmd
917
1214
  .command("create")
@@ -919,9 +1216,40 @@ usersCmd
919
1216
  .requiredOption("-t, --team <teamId>", "Team UUID")
920
1217
  .requiredOption("-j, --json <json>", "{ email, password, role? }")
921
1218
  .action(async (opts) => {
922
- const body = JSON.parse(opts.json);
923
- const res = await requireClient().createUser(opts.team, body);
924
- console.log(JSON.stringify(res, null, 2));
1219
+ await withPermissionGate({
1220
+ command: "users create",
1221
+ resource: "systemMember",
1222
+ action: "create",
1223
+ teamId: opts.team,
1224
+ }, async () => {
1225
+ const body = JSON.parse(opts.json);
1226
+ const res = await requireClient().createUser(opts.team, body);
1227
+ console.log(JSON.stringify(res, null, 2));
1228
+ });
1229
+ });
1230
+ usersCmd
1231
+ .command("system-create")
1232
+ .description("POST /v1/system/users")
1233
+ .requiredOption("-j, --json <json>", "{ email, name, password?, role? }")
1234
+ .option("--dry-run", "预览", false)
1235
+ .option("--apply", "执行写入", false)
1236
+ .action(async (opts) => {
1237
+ await withPermissionGate({
1238
+ command: "users system-create",
1239
+ resource: "systemMember",
1240
+ action: "create",
1241
+ }, async () => {
1242
+ const body = JSON.parse(opts.json);
1243
+ if (!assertApplyOrDryRun(opts, {
1244
+ command: "users system-create",
1245
+ danger: "write",
1246
+ summary: { email: body.email, name: body.name },
1247
+ })) {
1248
+ return;
1249
+ }
1250
+ const res = await requireClient().createSystemUser(body);
1251
+ console.log(JSON.stringify(res, null, 2));
1252
+ });
925
1253
  });
926
1254
  // --- brand-members ---
927
1255
  const brandMembersCmd = program.command("brand-members").description("品牌成员");
@@ -930,17 +1258,63 @@ brandMembersCmd
930
1258
  .description("GET /system/brand-members (preferred)")
931
1259
  .option("--user <uuid>", "userId 过滤")
932
1260
  .action(async (opts) => {
933
- const res = await requireClient().listSystemBrandMembers({ userId: opts.user });
934
- console.log(JSON.stringify(res, null, 2));
1261
+ await withPermissionGate({
1262
+ command: "brand-members system-list",
1263
+ resource: "systemBrand",
1264
+ action: "list",
1265
+ }, async () => {
1266
+ const res = await requireClient().listSystemBrandMembers({
1267
+ userId: opts.user,
1268
+ });
1269
+ console.log(JSON.stringify(res, null, 2));
1270
+ });
935
1271
  });
936
1272
  brandMembersCmd
937
1273
  .command("system-create")
938
1274
  .description("POST /system/brand-members (preferred)")
939
1275
  .requiredOption("-j, --json <json>", "{ brandId, userId, role? }")
940
- .action(async (opts) => {
941
- const body = JSON.parse(opts.json);
942
- const res = await requireClient().createSystemBrandMember(body);
943
- console.log(JSON.stringify(res, null, 2));
1276
+ .option("--dry-run", "预览", false)
1277
+ .option("--apply", "执行写入", false)
1278
+ .action(async (opts) => {
1279
+ await withPermissionGate({
1280
+ command: "brand-members system-create",
1281
+ resource: "brandMember",
1282
+ action: "create",
1283
+ }, async () => {
1284
+ const body = JSON.parse(opts.json);
1285
+ if (!assertApplyOrDryRun(opts, {
1286
+ command: "brand-members system-create",
1287
+ danger: "write",
1288
+ summary: body,
1289
+ })) {
1290
+ return;
1291
+ }
1292
+ const res = await requireClient().createSystemBrandMember(body);
1293
+ console.log(JSON.stringify(res, null, 2));
1294
+ });
1295
+ });
1296
+ brandMembersCmd
1297
+ .command("system-remove")
1298
+ .description("DELETE /system/brand-members/:memberId")
1299
+ .requiredOption("--id <memberId>", "BrandMember UUID")
1300
+ .option("--dry-run", "预览", false)
1301
+ .option("--apply", "执行写入", false)
1302
+ .action(async (opts) => {
1303
+ await withPermissionGate({
1304
+ command: "brand-members system-remove",
1305
+ resource: "brandMember",
1306
+ action: "delete",
1307
+ }, async () => {
1308
+ if (!assertApplyOrDryRun(opts, {
1309
+ command: "brand-members system-remove",
1310
+ danger: "delete",
1311
+ summary: { memberId: opts.id },
1312
+ })) {
1313
+ return;
1314
+ }
1315
+ await requireClient().deleteSystemBrandMember(opts.id);
1316
+ console.log("OK");
1317
+ });
944
1318
  });
945
1319
  brandMembersCmd
946
1320
  .command("list")
@@ -948,18 +1322,43 @@ brandMembersCmd
948
1322
  .requiredOption("-t, --team <teamId>", "Team UUID")
949
1323
  .option("--user <uuid>", "userId 过滤")
950
1324
  .action(async (opts) => {
951
- const res = await requireClient().listBrandMembers(opts.team, { userId: opts.user });
952
- console.log(JSON.stringify(res, null, 2));
1325
+ await withPermissionGate({
1326
+ command: "brand-members list",
1327
+ resource: "brandMember",
1328
+ action: "list",
1329
+ teamId: opts.team,
1330
+ }, async () => {
1331
+ const res = await requireClient().listBrandMembers(opts.team, {
1332
+ userId: opts.user,
1333
+ });
1334
+ console.log(JSON.stringify(res, null, 2));
1335
+ });
953
1336
  });
954
1337
  brandMembersCmd
955
1338
  .command("create")
956
1339
  .description("POST /brand-members")
957
1340
  .requiredOption("-t, --team <teamId>", "Team UUID")
958
1341
  .requiredOption("-j, --json <json>", "{ brandId, userId, role? }")
959
- .action(async (opts) => {
960
- const body = JSON.parse(opts.json);
961
- const res = await requireClient().createBrandMember(opts.team, body);
962
- console.log(JSON.stringify(res, null, 2));
1342
+ .option("--dry-run", "预览", false)
1343
+ .option("--apply", "执行写入", false)
1344
+ .action(async (opts) => {
1345
+ await withPermissionGate({
1346
+ command: "brand-members create",
1347
+ resource: "brandMember",
1348
+ action: "create",
1349
+ teamId: opts.team,
1350
+ }, async () => {
1351
+ const body = JSON.parse(opts.json);
1352
+ if (!assertApplyOrDryRun(opts, {
1353
+ command: "brand-members create",
1354
+ danger: "write",
1355
+ summary: { teamId: opts.team, ...body },
1356
+ })) {
1357
+ return;
1358
+ }
1359
+ const res = await requireClient().createBrandMember(opts.team, body);
1360
+ console.log(JSON.stringify(res, null, 2));
1361
+ });
963
1362
  });
964
1363
  // --- notification-channels ---
965
1364
  const notifChannels = program
@@ -1023,56 +1422,147 @@ apiKeys
1023
1422
  .requiredOption("-t, --team <teamId>", "Team UUID")
1024
1423
  .option("-n, --limit <n>", "limit", "50")
1025
1424
  .action(async (opts) => {
1026
- const res = await requireClient().listApiKeys(opts.team, { limit: Number(opts.limit) });
1027
- console.log(JSON.stringify(res, null, 2));
1425
+ await withPermissionGate({
1426
+ command: "api-keys list",
1427
+ resource: "apiKey",
1428
+ action: "list",
1429
+ teamId: opts.team,
1430
+ }, async () => {
1431
+ const res = await requireClient().listApiKeys(opts.team, {
1432
+ limit: Number(opts.limit),
1433
+ });
1434
+ console.log(JSON.stringify(res, null, 2));
1435
+ });
1028
1436
  });
1029
1437
  apiKeys
1030
1438
  .command("create")
1031
1439
  .description("POST /api-keys")
1032
1440
  .requiredOption("-t, --team <teamId>", "Team UUID")
1033
1441
  .requiredOption("-j, --json <json>", "{ name, role, visibleCampaignIds? }")
1034
- .action(async (opts) => {
1035
- const body = JSON.parse(opts.json);
1036
- const res = await requireClient().createApiKey(opts.team, body);
1037
- console.log(JSON.stringify(res, null, 2));
1442
+ .option("--dry-run", "预览", false)
1443
+ .option("--apply", "执行写入", false)
1444
+ .action(async (opts) => {
1445
+ await withPermissionGate({
1446
+ command: "api-keys create",
1447
+ resource: "apiKey",
1448
+ action: "create",
1449
+ teamId: opts.team,
1450
+ }, async () => {
1451
+ const body = JSON.parse(opts.json);
1452
+ if (!assertApplyOrDryRun(opts, {
1453
+ command: "api-keys create",
1454
+ danger: "secret",
1455
+ summary: { teamId: opts.team, ...body },
1456
+ })) {
1457
+ return;
1458
+ }
1459
+ const res = await requireClient().createApiKey(opts.team, body);
1460
+ console.log(JSON.stringify(res, null, 2));
1461
+ });
1038
1462
  });
1039
1463
  apiKeys
1040
1464
  .command("delete")
1041
1465
  .description("DELETE /api-keys/:id")
1042
1466
  .requiredOption("-t, --team <teamId>", "Team UUID")
1043
1467
  .requiredOption("--key <uuid>", "API Key UUID")
1044
- .action(async (opts) => {
1045
- await requireClient().deleteApiKey(opts.team, opts.key);
1046
- console.log("OK");
1468
+ .option("--dry-run", "预览", false)
1469
+ .option("--apply", "执行写入", false)
1470
+ .action(async (opts) => {
1471
+ await withPermissionGate({
1472
+ command: "api-keys delete",
1473
+ resource: "apiKey",
1474
+ action: "delete",
1475
+ teamId: opts.team,
1476
+ }, async () => {
1477
+ if (!assertApplyOrDryRun(opts, {
1478
+ command: "api-keys delete",
1479
+ danger: "delete",
1480
+ summary: { teamId: opts.team, keyId: opts.key },
1481
+ })) {
1482
+ return;
1483
+ }
1484
+ await requireClient().deleteApiKey(opts.team, opts.key);
1485
+ console.log("OK");
1486
+ });
1047
1487
  });
1048
1488
  apiKeys
1049
1489
  .command("rotate")
1050
1490
  .description("POST /api-keys/:id/rotate")
1051
1491
  .requiredOption("-t, --team <teamId>", "Team UUID")
1052
1492
  .requiredOption("--key <uuid>", "API Key UUID")
1053
- .action(async (opts) => {
1054
- const res = await requireClient().rotateApiKey(opts.team, opts.key);
1055
- console.log(JSON.stringify(res, null, 2));
1493
+ .option("--dry-run", "预览", false)
1494
+ .option("--apply", "执行写入", false)
1495
+ .action(async (opts) => {
1496
+ await withPermissionGate({
1497
+ command: "api-keys rotate",
1498
+ resource: "apiKey",
1499
+ action: "update",
1500
+ teamId: opts.team,
1501
+ }, async () => {
1502
+ if (!assertApplyOrDryRun(opts, {
1503
+ command: "api-keys rotate",
1504
+ danger: "secret",
1505
+ summary: { teamId: opts.team, keyId: opts.key },
1506
+ })) {
1507
+ return;
1508
+ }
1509
+ const res = await requireClient().rotateApiKey(opts.team, opts.key);
1510
+ console.log(JSON.stringify(res, null, 2));
1511
+ });
1056
1512
  });
1057
1513
  apiKeys
1058
1514
  .command("batch-revoke")
1059
1515
  .description("POST /api-keys/batch/revoke")
1060
1516
  .requiredOption("-t, --team <teamId>", "Team UUID")
1061
- .requiredOption("-j, --json <json>", '{ "ids": ["uuid1","uuid2"] }')
1062
- .action(async (opts) => {
1063
- const body = JSON.parse(opts.json);
1064
- const res = await requireClient().batchRevokeApiKeys(opts.team, body.ids);
1065
- console.log(JSON.stringify(res, null, 2));
1517
+ .requiredOption("-j, --json <json>", '{ "apiKeyIds": ["uuid1","uuid2"] }')
1518
+ .option("--dry-run", "预览", false)
1519
+ .option("--apply", "执行写入", false)
1520
+ .action(async (opts) => {
1521
+ await withPermissionGate({
1522
+ command: "api-keys batch-revoke",
1523
+ resource: "apiKey",
1524
+ action: "delete",
1525
+ teamId: opts.team,
1526
+ }, async () => {
1527
+ const body = JSON.parse(opts.json);
1528
+ const apiKeyIds = body.apiKeyIds ?? body.ids ?? [];
1529
+ if (!assertApplyOrDryRun(opts, {
1530
+ command: "api-keys batch-revoke",
1531
+ danger: "delete",
1532
+ summary: { teamId: opts.team, apiKeyIds },
1533
+ })) {
1534
+ return;
1535
+ }
1536
+ const res = await requireClient().batchRevokeApiKeys(opts.team, apiKeyIds);
1537
+ console.log(JSON.stringify(res, null, 2));
1538
+ });
1066
1539
  });
1067
1540
  apiKeys
1068
1541
  .command("batch-rotate")
1069
1542
  .description("POST /api-keys/batch/rotate")
1070
1543
  .requiredOption("-t, --team <teamId>", "Team UUID")
1071
- .requiredOption("-j, --json <json>", '{ "ids": ["uuid1","uuid2"] }')
1072
- .action(async (opts) => {
1073
- const body = JSON.parse(opts.json);
1074
- const res = await requireClient().batchRotateApiKeys(opts.team, body.ids);
1075
- console.log(JSON.stringify(res, null, 2));
1544
+ .requiredOption("-j, --json <json>", '{ "apiKeyIds": ["uuid1","uuid2"] }')
1545
+ .option("--dry-run", "预览", false)
1546
+ .option("--apply", "执行写入", false)
1547
+ .action(async (opts) => {
1548
+ await withPermissionGate({
1549
+ command: "api-keys batch-rotate",
1550
+ resource: "apiKey",
1551
+ action: "update",
1552
+ teamId: opts.team,
1553
+ }, async () => {
1554
+ const body = JSON.parse(opts.json);
1555
+ const apiKeyIds = body.apiKeyIds ?? body.ids ?? [];
1556
+ if (!assertApplyOrDryRun(opts, {
1557
+ command: "api-keys batch-rotate",
1558
+ danger: "secret",
1559
+ summary: { teamId: opts.team, apiKeyIds },
1560
+ })) {
1561
+ return;
1562
+ }
1563
+ const res = await requireClient().batchRotateApiKeys(opts.team, apiKeyIds);
1564
+ console.log(JSON.stringify(res, null, 2));
1565
+ });
1076
1566
  });
1077
1567
  // ═══════════════════════════════════════════════════════════════════════════
1078
1568
  // Group C: scattered endpoint additions
@@ -1127,9 +1617,16 @@ program
1127
1617
  .requiredOption("-t, --team <teamId>", "Team UUID")
1128
1618
  .requiredOption("-j, --json <json>", "权限矩阵更新 body")
1129
1619
  .action(async (opts) => {
1130
- const body = JSON.parse(opts.json);
1131
- const res = await requireClient().updatePermissionsMatrix(opts.team, body);
1132
- console.log(JSON.stringify(res, null, 2));
1620
+ await withPermissionGate({
1621
+ command: "permissions-update",
1622
+ resource: "permissionMatrix",
1623
+ action: "update",
1624
+ teamId: opts.team,
1625
+ }, async () => {
1626
+ const body = JSON.parse(opts.json);
1627
+ const res = await requireClient().updatePermissionsMatrix(opts.team, body);
1628
+ console.log(JSON.stringify(res, null, 2));
1629
+ });
1133
1630
  });
1134
1631
  graph
1135
1632
  .command("drilldown")
@@ -1144,6 +1641,114 @@ graph
1144
1641
  });
1145
1642
  console.log(JSON.stringify(res, null, 2));
1146
1643
  });
1644
+ graph
1645
+ .command("v2-ego")
1646
+ .description("GET /graph/v2/ego")
1647
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1648
+ .option("--node-id <uuid>", "graph nodeId")
1649
+ .option("--node-ref <id>", "nodeRefId (e.g. account UUID)")
1650
+ .option("--node-type <type>", "account|team|campaign")
1651
+ .option("--relation-type <type>", "relationType filter")
1652
+ .option("--edge-kind <kind>", "edgeKind filter")
1653
+ .option("-n, --limit <n>", "limit", "100")
1654
+ .action(async (opts) => {
1655
+ const res = await requireClient().getGraphV2Ego(opts.team, {
1656
+ nodeId: opts.nodeId,
1657
+ nodeRefId: opts.nodeRef,
1658
+ nodeType: opts.nodeType,
1659
+ relationType: opts.relationType,
1660
+ edgeKind: opts.edgeKind,
1661
+ limit: Number(opts.limit),
1662
+ });
1663
+ console.log(JSON.stringify(res, null, 2));
1664
+ });
1665
+ graph
1666
+ .command("v2-timeline")
1667
+ .description("GET /graph/v2/timeline")
1668
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1669
+ .option("--node-id <uuid>", "graph nodeId")
1670
+ .option("--node-ref <id>", "nodeRefId")
1671
+ .option("--node-type <type>", "account|team|campaign")
1672
+ .option("--from <iso>", "from ISO time")
1673
+ .option("--to <iso>", "to ISO time")
1674
+ .option("-n, --limit <n>", "limit", "100")
1675
+ .action(async (opts) => {
1676
+ const res = await requireClient().getGraphV2Timeline(opts.team, {
1677
+ nodeId: opts.nodeId,
1678
+ nodeRefId: opts.nodeRef,
1679
+ nodeType: opts.nodeType,
1680
+ from: opts.from,
1681
+ to: opts.to,
1682
+ limit: Number(opts.limit),
1683
+ });
1684
+ console.log(JSON.stringify(res, null, 2));
1685
+ });
1686
+ graph
1687
+ .command("v2-explain")
1688
+ .description("GET /graph/v2/explain/:edgeId")
1689
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1690
+ .requiredOption("--edge-id <id>", "edge UUID")
1691
+ .action(async (opts) => {
1692
+ const res = await requireClient().getGraphV2Explain(opts.team, opts.edgeId);
1693
+ console.log(JSON.stringify(res, null, 2));
1694
+ });
1695
+ graph
1696
+ .command("v2-path")
1697
+ .description("GET /graph/v2/path")
1698
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1699
+ .option("--source-id <uuid>", "source nodeId")
1700
+ .option("--source-ref <id>", "source nodeRefId")
1701
+ .option("--source-type <type>", "source nodeType")
1702
+ .option("--target-id <uuid>", "target nodeId")
1703
+ .option("--target-ref <id>", "target nodeRefId")
1704
+ .option("--target-type <type>", "target nodeType")
1705
+ .option("--max-depth <n>", "maxDepth", "4")
1706
+ .option("-n, --limit <n>", "limit", "50")
1707
+ .action(async (opts) => {
1708
+ const res = await requireClient().getGraphV2Path(opts.team, {
1709
+ sourceNodeId: opts.sourceId,
1710
+ sourceNodeRefId: opts.sourceRef,
1711
+ sourceNodeType: opts.sourceType,
1712
+ targetNodeId: opts.targetId,
1713
+ targetNodeRefId: opts.targetRef,
1714
+ targetNodeType: opts.targetType,
1715
+ maxDepth: Number(opts.maxDepth),
1716
+ limit: Number(opts.limit),
1717
+ });
1718
+ console.log(JSON.stringify(res, null, 2));
1719
+ });
1720
+ graph
1721
+ .command("v2-clusters")
1722
+ .description("GET /graph/v2/clusters")
1723
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1724
+ .option("--scope-ref <id>", "scopeNodeRefId")
1725
+ .option("--scope-type <type>", "scopeNodeType", "account")
1726
+ .option("--min-risk <n>", "minRiskScore", "60")
1727
+ .option("-n, --limit <n>", "limit", "100")
1728
+ .action(async (opts) => {
1729
+ const res = await requireClient().getGraphV2Clusters(opts.team, {
1730
+ scopeNodeRefId: opts.scopeRef,
1731
+ scopeNodeType: opts.scopeType,
1732
+ minRiskScore: Number(opts.minRisk),
1733
+ limit: Number(opts.limit),
1734
+ });
1735
+ console.log(JSON.stringify(res, null, 2));
1736
+ });
1737
+ graph
1738
+ .command("v2-impact")
1739
+ .description("GET /graph/v2/impact")
1740
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1741
+ .option("--campaign-ref <id>", "campaignNodeRefId")
1742
+ .option("--min-reuse <n>", "minReuseCount", "2")
1743
+ .option("-n, --limit <n>", "limit", "100")
1744
+ .action(async (opts) => {
1745
+ const res = await requireClient().getGraphV2Impact(opts.team, {
1746
+ campaignNodeRefId: opts.campaignRef,
1747
+ minReuseCount: Number(opts.minReuse),
1748
+ limit: Number(opts.limit),
1749
+ });
1750
+ console.log(JSON.stringify(res, null, 2));
1751
+ });
1147
1752
  // ═══════════════════════════════════════════════════════════════════════════
1148
1753
  // Group D: add filter params to existing list commands
1149
1754
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1380,9 +1985,25 @@ brandMembersCmd
1380
1985
  .description("DELETE /brand-members/:memberId")
1381
1986
  .requiredOption("-t, --team <teamId>", "Team UUID")
1382
1987
  .requiredOption("-m, --member <memberId>", "BrandMember UUID")
1383
- .action(async (opts) => {
1384
- const res = await requireClient().deleteBrandMember(opts.team, opts.member);
1385
- console.log(JSON.stringify(res, null, 2));
1988
+ .option("--dry-run", "预览", false)
1989
+ .option("--apply", "执行写入", false)
1990
+ .action(async (opts) => {
1991
+ await withPermissionGate({
1992
+ command: "brand-members remove",
1993
+ resource: "brandMember",
1994
+ action: "delete",
1995
+ teamId: opts.team,
1996
+ }, async () => {
1997
+ if (!assertApplyOrDryRun(opts, {
1998
+ command: "brand-members remove",
1999
+ danger: "delete",
2000
+ summary: { teamId: opts.team, memberId: opts.member },
2001
+ })) {
2002
+ return;
2003
+ }
2004
+ const res = await requireClient().deleteBrandMember(opts.team, opts.member);
2005
+ console.log(JSON.stringify(res, null, 2));
2006
+ });
1386
2007
  });
1387
2008
  // Only parse when run directly (not when imported for testing)
1388
2009
  if (!process.env["VITEST"]) {