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

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 (55) hide show
  1. package/CHANGELOG.md +16 -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 +810 -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 +26 -19
  35. package/dist/register-extensions.js.map +1 -1
  36. package/dist/register-shared.js +1 -1
  37. package/dist/register-shared.js.map +1 -1
  38. package/package.json +2 -2
  39. package/skills/README.md +7 -5
  40. package/skills/manifest.json +17 -7
  41. package/skills/social-hub-accounts/SKILL.md +46 -13
  42. package/skills/social-hub-admin/SKILL.md +76 -10
  43. package/skills/social-hub-calendar-jobs/SKILL.md +26 -10
  44. package/skills/social-hub-cli/SKILL.md +35 -191
  45. package/skills/social-hub-cli/evals/evals.json +4 -4
  46. package/skills/social-hub-events-observability/SKILL.md +50 -0
  47. package/skills/social-hub-graph-compliance/SKILL.md +60 -0
  48. package/skills/social-hub-intelligence/SKILL.md +72 -7
  49. package/skills/social-hub-migration/SKILL.md +18 -5
  50. package/skills/social-hub-openclaw-context/SKILL.md +23 -8
  51. package/skills/social-hub-ops-runtime/SKILL.md +10 -5
  52. package/skills/social-hub-posts/SKILL.md +56 -23
  53. package/skills/social-hub-posts/evals/evals.json +23 -0
  54. package/skills/social-hub-publishing/SKILL.md +75 -11
  55. 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";
@@ -44,6 +46,12 @@ events
44
46
  .command("append")
45
47
  .requiredOption("-t, --team <teamId>", "Team UUID")
46
48
  .requiredOption("--type <type>", "事件类型,如 comment")
49
+ .option("--external-ref <ref>", "幂等键(也可放在 payload.externalRef / event_id)")
50
+ .option("--account <uuid>", "socialAccountId")
51
+ .option("--subreddit <name>", "subreddit")
52
+ .option("--result <result>", "succeeded | failed | skipped")
53
+ .option("--target-url <url>", "目标 URL / permalink")
54
+ .option("--actor <actor>", "user | agent | cron")
47
55
  .option("--payload <json>", "JSON 对象", "{}")
48
56
  .action(async (opts) => {
49
57
  let payload = {};
@@ -56,6 +64,30 @@ events
56
64
  }
57
65
  const res = await requireClient().appendEvent(opts.team, {
58
66
  type: opts.type,
67
+ externalRef: opts.externalRef ??
68
+ (typeof payload.externalRef === "string" ? payload.externalRef : undefined) ??
69
+ (typeof payload.event_id === "string" ? payload.event_id : undefined),
70
+ socialAccountId: opts.account ??
71
+ (typeof payload.socialAccountId === "string"
72
+ ? payload.socialAccountId
73
+ : undefined),
74
+ subreddit: opts.subreddit ??
75
+ (typeof payload.subreddit === "string" ? payload.subreddit : undefined),
76
+ result: opts.result ??
77
+ (payload.result === "succeeded" ||
78
+ payload.result === "failed" ||
79
+ payload.result === "skipped"
80
+ ? payload.result
81
+ : undefined),
82
+ targetUrl: opts.targetUrl ??
83
+ (typeof payload.targetUrl === "string" ? payload.targetUrl : undefined) ??
84
+ (typeof payload.permalink === "string" ? payload.permalink : undefined),
85
+ actor: opts.actor ??
86
+ (payload.actor === "user" ||
87
+ payload.actor === "agent" ||
88
+ payload.actor === "cron"
89
+ ? payload.actor
90
+ : undefined),
59
91
  payload,
60
92
  });
61
93
  console.log(JSON.stringify(res, null, 2));
@@ -85,6 +117,102 @@ events
85
117
  const res = await requireClient().appendEventsBatch(opts.team, body);
86
118
  console.log(JSON.stringify(res, null, 2));
87
119
  });
120
+ events
121
+ .command("import-openclaw-history")
122
+ .description("POST /interaction-history/import — OpenClaw v1 JSON 幂等入库")
123
+ .requiredOption("-t, --team <teamId>", "Team UUID")
124
+ .requiredOption("-f, --file <path>", "interaction-history.json 路径")
125
+ .option("--dry-run", "只校验 schema/slot/幂等,不写库")
126
+ .option("--no-auto-mark", "关闭 auto_metrics 阈值补 mark")
127
+ .action(async (opts) => {
128
+ const fs = await import("node:fs/promises");
129
+ const raw = await fs.readFile(opts.file, "utf8");
130
+ let parsed;
131
+ try {
132
+ parsed = JSON.parse(raw);
133
+ }
134
+ catch {
135
+ console.error("Invalid JSON file");
136
+ process.exit(1);
137
+ }
138
+ const res = await requireClient().importInteractionHistory(opts.team, {
139
+ ...parsed,
140
+ sourceFile: opts.file,
141
+ dryRun: Boolean(opts.dryRun),
142
+ autoMarkThreshold: !opts.noAutoMark,
143
+ });
144
+ console.log(JSON.stringify(res, null, 2));
145
+ });
146
+ const styleMarks = program.command("style-marks").description("风格反馈标记");
147
+ styleMarks
148
+ .command("list")
149
+ .requiredOption("-t, --team <teamId>", "Team UUID")
150
+ .option("--account <uuid>", "socialAccountId")
151
+ .option("--extract-status <st>", "pending | extracted | rejected | skipped | failed")
152
+ .option("--approved <bool>", "true | false")
153
+ .option("-n, --limit <n>", "limit", "50")
154
+ .option("--offset <n>", "offset", "0")
155
+ .action(async (opts) => {
156
+ const res = await requireClient().listStyleMarks(opts.team, {
157
+ socialAccountId: opts.account,
158
+ extractStatus: opts.extractStatus,
159
+ approved: opts.approved === undefined ? undefined : opts.approved === "true",
160
+ limit: Number(opts.limit),
161
+ offset: Number(opts.offset),
162
+ });
163
+ console.log(JSON.stringify(res, null, 2));
164
+ });
165
+ styleMarks
166
+ .command("approve")
167
+ .requiredOption("-t, --team <teamId>", "Team UUID")
168
+ .requiredOption("--id <styleMarkId>", "Style mark UUID")
169
+ .option("--note <text>", "Reviewer note")
170
+ .action(async (opts) => {
171
+ const res = await requireClient().patchStyleMark(opts.team, opts.id, {
172
+ approved: true,
173
+ extractStatus: "pending",
174
+ note: opts.note,
175
+ });
176
+ console.log(JSON.stringify(res, null, 2));
177
+ });
178
+ styleMarks
179
+ .command("reject")
180
+ .requiredOption("-t, --team <teamId>", "Team UUID")
181
+ .requiredOption("--id <styleMarkId>", "Style mark UUID")
182
+ .option("--note <text>", "Reviewer note")
183
+ .action(async (opts) => {
184
+ const res = await requireClient().patchStyleMark(opts.team, opts.id, {
185
+ approved: false,
186
+ extractStatus: "rejected",
187
+ note: opts.note,
188
+ });
189
+ console.log(JSON.stringify(res, null, 2));
190
+ });
191
+ styleMarks
192
+ .command("extract")
193
+ .requiredOption("-t, --team <teamId>", "Team UUID")
194
+ .requiredOption("--id <styleMarkId>", "Style mark UUID")
195
+ .option("--dry-run", "只预览提取结果")
196
+ .action(async (opts) => {
197
+ const res = await requireClient().extractStyleMark(opts.team, opts.id, {
198
+ dryRun: Boolean(opts.dryRun),
199
+ });
200
+ console.log(JSON.stringify(res, null, 2));
201
+ });
202
+ const curator = program.command("curator").description("风格沉淀 curator");
203
+ curator
204
+ .command("run")
205
+ .requiredOption("-t, --team <teamId>", "Team UUID")
206
+ .requiredOption("--account <ref>", "账号 UUID 或 handle/slot")
207
+ .option("--dry-run", "只预览")
208
+ .option("-n, --limit <n>", "最多处理 pending marks", "50")
209
+ .action(async (opts) => {
210
+ const res = await requireClient().runStyleCurator(opts.team, opts.account, {
211
+ dryRun: Boolean(opts.dryRun),
212
+ limit: Number(opts.limit),
213
+ });
214
+ console.log(JSON.stringify(res, null, 2));
215
+ });
88
216
  // --- dashboard ---
89
217
  const dashboard = program.command("dashboard").description("运营总览");
90
218
  dashboard
@@ -604,6 +732,28 @@ compliance
604
732
  const res = await requireClient().upsertAccountRiskProfile(opts.team, opts.account, body);
605
733
  console.log(JSON.stringify(res, null, 2));
606
734
  });
735
+ compliance
736
+ .command("rule-caches-list")
737
+ .description("GET /subreddit-rule-caches")
738
+ .requiredOption("-t, --team <teamId>", "Team UUID")
739
+ .option("--include-expired", "include expired caches")
740
+ .action(async (opts) => {
741
+ const res = await requireClient().listSubredditRuleCaches(opts.team, {
742
+ includeExpired: Boolean(opts.includeExpired),
743
+ });
744
+ console.log(JSON.stringify(res, null, 2));
745
+ });
746
+ compliance
747
+ .command("rule-caches-upsert")
748
+ .description("PUT /subreddit-rule-caches/:subreddit")
749
+ .requiredOption("-t, --team <teamId>", "Team UUID")
750
+ .requiredOption("--sub <name>", "subreddit name")
751
+ .requiredOption("-j, --json <json>", "{ rulesMarkdown, sourceUrl?, expiresInDays? }")
752
+ .action(async (opts) => {
753
+ const body = JSON.parse(opts.json);
754
+ const res = await requireClient().upsertSubredditRuleCache(opts.team, opts.sub, body);
755
+ console.log(JSON.stringify(res, null, 2));
756
+ });
607
757
  // --- openclaw ---
608
758
  program
609
759
  .command("openclaw-ingest")
@@ -716,8 +866,14 @@ agentTeams
716
866
  .command("list")
717
867
  .description("GET /v1/agent-teams")
718
868
  .action(async () => {
719
- const res = await requireClient().listAgentTeams();
720
- console.log(JSON.stringify(res, null, 2));
869
+ await withPermissionGate({
870
+ command: "agent-teams list",
871
+ resource: "agentTeam",
872
+ action: "list",
873
+ }, async () => {
874
+ const res = await requireClient().listAgentTeams();
875
+ console.log(JSON.stringify(res, null, 2));
876
+ });
721
877
  });
722
878
  agentTeams
723
879
  .command("workspace-docs")
@@ -726,30 +882,71 @@ agentTeams
726
882
  .option("--kind <kind>", "file|daily_report|weekly_report|ops_record")
727
883
  .option("-n, --limit <n>", "limit", "50")
728
884
  .action(async (opts) => {
729
- const res = await requireClient().listWorkspaceDocuments(opts.team, {
730
- kind: opts.kind,
731
- limit: Number(opts.limit),
885
+ await withPermissionGate({
886
+ command: "agent-teams workspace-docs",
887
+ resource: "agentTeam",
888
+ action: "read",
889
+ teamId: opts.team,
890
+ }, async () => {
891
+ const res = await requireClient().listWorkspaceDocuments(opts.team, {
892
+ kind: opts.kind,
893
+ limit: Number(opts.limit),
894
+ });
895
+ console.log(JSON.stringify(res, null, 2));
732
896
  });
733
- console.log(JSON.stringify(res, null, 2));
734
897
  });
735
898
  agentTeams
736
899
  .command("create")
737
900
  .description("POST /v1/agent-teams — 新建团队(admin only)")
738
901
  .requiredOption("--slug <slug>", "团队 slug(小写字母、数字、横线)")
739
902
  .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));
903
+ .option("--dry-run", "预览", false)
904
+ .option("--apply", "执行写入", false)
905
+ .action(async (opts) => {
906
+ await withPermissionGate({
907
+ command: "agent-teams create",
908
+ resource: "agentTeam",
909
+ action: "create",
910
+ }, async () => {
911
+ if (!assertApplyOrDryRun(opts, {
912
+ command: "agent-teams create",
913
+ danger: "admin",
914
+ summary: { slug: opts.slug, name: opts.name },
915
+ })) {
916
+ return;
917
+ }
918
+ const res = await requireClient().createTeam({
919
+ slug: opts.slug,
920
+ name: opts.name,
921
+ });
922
+ console.log(JSON.stringify(res, null, 2));
923
+ });
743
924
  });
744
925
  agentTeams
745
926
  .command("update")
746
927
  .description("PATCH /v1/agent-teams/:teamId — 更新团队(admin only)")
747
928
  .requiredOption("-t, --team <teamId>", "Team UUID")
748
929
  .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));
930
+ .option("--dry-run", "预览", false)
931
+ .option("--apply", "执行写入", false)
932
+ .action(async (opts) => {
933
+ await withPermissionGate({
934
+ command: "agent-teams update",
935
+ resource: "agentTeam",
936
+ action: "update",
937
+ teamId: opts.team,
938
+ }, async () => {
939
+ const body = JSON.parse(opts.json);
940
+ if (!assertApplyOrDryRun(opts, {
941
+ command: "agent-teams update",
942
+ danger: "admin",
943
+ summary: { teamId: opts.team, ...body },
944
+ })) {
945
+ return;
946
+ }
947
+ const res = await requireClient().updateTeam(opts.team, body);
948
+ console.log(JSON.stringify(res, null, 2));
949
+ });
753
950
  });
754
951
  agentTeams
755
952
  .command("add-member")
@@ -757,12 +954,32 @@ agentTeams
757
954
  .requiredOption("-t, --team <teamId>", "Team UUID")
758
955
  .requiredOption("--user <userId>", "User UUID")
759
956
  .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,
957
+ .option("--dry-run", "预览", false)
958
+ .option("--apply", "执行写入", false)
959
+ .action(async (opts) => {
960
+ await withPermissionGate({
961
+ command: "agent-teams add-member",
962
+ resource: "agentTeam",
963
+ action: "update",
964
+ teamId: opts.team,
965
+ }, async () => {
966
+ if (!assertApplyOrDryRun(opts, {
967
+ command: "agent-teams add-member",
968
+ danger: "admin",
969
+ summary: {
970
+ teamId: opts.team,
971
+ userId: opts.user,
972
+ role: opts.role,
973
+ },
974
+ })) {
975
+ return;
976
+ }
977
+ const res = await requireClient().addTeamMember(opts.team, {
978
+ userId: opts.user,
979
+ role: opts.role,
980
+ });
981
+ console.log(JSON.stringify(res, null, 2));
764
982
  });
765
- console.log(JSON.stringify(res, null, 2));
766
983
  });
767
984
  // --- intelligence (subreddit-intelligence) ---
768
985
  const intel = program.command("intelligence").description("板块情报与 KOL 挖掘");
@@ -903,6 +1120,192 @@ intel
903
1120
  const res = await requireClient().dispatchTaskFromHotPost(opts.team, body);
904
1121
  console.log(JSON.stringify(res, null, 2));
905
1122
  });
1123
+ intel
1124
+ .command("tier-rules-list")
1125
+ .description("GET .../subreddit-intelligence/tier-rules")
1126
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1127
+ .option("--tier <tier>", "tier filter")
1128
+ .option("--enabled <bool>", "true|false")
1129
+ .option("-n, --limit <n>", "limit", "50")
1130
+ .option("--offset <n>", "offset", "0")
1131
+ .action(async (opts) => {
1132
+ const res = await requireClient().listTierRules(opts.team, {
1133
+ tier: opts.tier,
1134
+ enabled: opts.enabled === undefined
1135
+ ? undefined
1136
+ : opts.enabled === "true" || opts.enabled === "1",
1137
+ limit: Number(opts.limit),
1138
+ offset: Number(opts.offset),
1139
+ });
1140
+ console.log(JSON.stringify(res, null, 2));
1141
+ });
1142
+ intel
1143
+ .command("tier-rules-create")
1144
+ .description("POST .../subreddit-intelligence/tier-rules")
1145
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1146
+ .requiredOption("-j, --json <json>", "tier rule body")
1147
+ .action(async (opts) => {
1148
+ const body = JSON.parse(opts.json);
1149
+ const res = await requireClient().createTierRule(opts.team, body);
1150
+ console.log(JSON.stringify(res, null, 2));
1151
+ });
1152
+ intel
1153
+ .command("tier-rules-update")
1154
+ .description("PATCH .../subreddit-intelligence/tier-rules/:id")
1155
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1156
+ .requiredOption("--id <uuid>", "Tier rule UUID")
1157
+ .requiredOption("-j, --json <json>", "update body")
1158
+ .action(async (opts) => {
1159
+ const body = JSON.parse(opts.json);
1160
+ const res = await requireClient().updateTierRule(opts.team, opts.id, body);
1161
+ console.log(JSON.stringify(res, null, 2));
1162
+ });
1163
+ intel
1164
+ .command("tier-rules-delete")
1165
+ .description("DELETE .../subreddit-intelligence/tier-rules/:id")
1166
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1167
+ .requiredOption("--id <uuid>", "Tier rule UUID")
1168
+ .action(async (opts) => {
1169
+ const res = await requireClient().deleteTierRule(opts.team, opts.id);
1170
+ console.log(JSON.stringify(res, null, 2));
1171
+ });
1172
+ intel
1173
+ .command("runs-list")
1174
+ .description("GET .../subreddit-intelligence/runs")
1175
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1176
+ .option("--dimension <d>", "industry|keywords|tier")
1177
+ .option("--status <s>", "queued|running|succeeded|failed")
1178
+ .option("-n, --limit <n>", "limit", "50")
1179
+ .option("--offset <n>", "offset", "0")
1180
+ .action(async (opts) => {
1181
+ const res = await requireClient().listIntelRuns(opts.team, {
1182
+ dimension: opts.dimension,
1183
+ status: opts.status,
1184
+ limit: Number(opts.limit),
1185
+ offset: Number(opts.offset),
1186
+ });
1187
+ console.log(JSON.stringify(res, null, 2));
1188
+ });
1189
+ intel
1190
+ .command("kol-profiles-list")
1191
+ .description("GET .../subreddit-intelligence/kol-profiles")
1192
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1193
+ .option("--platform <p>", "reddit")
1194
+ .option("-n, --limit <n>", "limit", "50")
1195
+ .option("--offset <n>", "offset", "0")
1196
+ .action(async (opts) => {
1197
+ const res = await requireClient().listKolProfiles(opts.team, {
1198
+ platform: opts.platform,
1199
+ limit: Number(opts.limit),
1200
+ offset: Number(opts.offset),
1201
+ });
1202
+ console.log(JSON.stringify(res, null, 2));
1203
+ });
1204
+ intel
1205
+ .command("kol-profiles-get")
1206
+ .description("GET .../subreddit-intelligence/kol-profiles/:id")
1207
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1208
+ .requiredOption("--id <uuid>", "KOL profile UUID")
1209
+ .action(async (opts) => {
1210
+ const res = await requireClient().getKolProfile(opts.team, opts.id);
1211
+ console.log(JSON.stringify(res, null, 2));
1212
+ });
1213
+ intel
1214
+ .command("fetch-by-industry")
1215
+ .description("POST .../subreddit-intelligence/hot-posts/fetch-by-industry")
1216
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1217
+ .requiredOption("-j, --json <json>", "fetch body")
1218
+ .action(async (opts) => {
1219
+ const body = JSON.parse(opts.json);
1220
+ const res = await requireClient().fetchHotPostsByIndustry(opts.team, body);
1221
+ console.log(JSON.stringify(res, null, 2));
1222
+ });
1223
+ intel
1224
+ .command("fetch-by-tier")
1225
+ .description("POST .../subreddit-intelligence/hot-posts/fetch-by-tier")
1226
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1227
+ .requiredOption("-j, --json <json>", "fetch body")
1228
+ .action(async (opts) => {
1229
+ const body = JSON.parse(opts.json);
1230
+ const res = await requireClient().fetchHotPostsByTier(opts.team, body);
1231
+ console.log(JSON.stringify(res, null, 2));
1232
+ });
1233
+ intel
1234
+ .command("fetch-by-keywords")
1235
+ .description("POST .../subreddit-intelligence/insights/fetch-by-keywords")
1236
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1237
+ .requiredOption("-j, --json <json>", "fetch body")
1238
+ .action(async (opts) => {
1239
+ const body = JSON.parse(opts.json);
1240
+ const res = await requireClient().fetchInsightsByKeywords(opts.team, body);
1241
+ console.log(JSON.stringify(res, null, 2));
1242
+ });
1243
+ intel
1244
+ .command("brand-mention-radar")
1245
+ .description("POST .../subreddit-intelligence/insights/brand-mention-radar")
1246
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1247
+ .requiredOption("-j, --json <json>", "{ keywords, minScore?, minComments? }")
1248
+ .action(async (opts) => {
1249
+ const body = JSON.parse(opts.json);
1250
+ const res = await requireClient().getBrandMentionRadar(opts.team, body);
1251
+ console.log(JSON.stringify(res, null, 2));
1252
+ });
1253
+ intel
1254
+ .command("opportunity-map")
1255
+ .description("POST .../subreddit-intelligence/insights/opportunity-map")
1256
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1257
+ .requiredOption("-j, --json <json>", "{ keywords, minScore?, minComments? }")
1258
+ .action(async (opts) => {
1259
+ const body = JSON.parse(opts.json);
1260
+ const res = await requireClient().getOpportunityMap(opts.team, body);
1261
+ console.log(JSON.stringify(res, null, 2));
1262
+ });
1263
+ intel
1264
+ .command("sov")
1265
+ .description("POST .../subreddit-intelligence/insights/sov")
1266
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1267
+ .requiredOption("-j, --json <json>", "{ brandKeywords, competitorKeywords, minScore?, minComments? }")
1268
+ .action(async (opts) => {
1269
+ const body = JSON.parse(opts.json);
1270
+ const res = await requireClient().getInsightsSov(opts.team, body);
1271
+ console.log(JSON.stringify(res, null, 2));
1272
+ });
1273
+ intel
1274
+ .command("sentiment-intent")
1275
+ .description("GET .../subreddit-intelligence/insights/sentiment-intent")
1276
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1277
+ .action(async (opts) => {
1278
+ const res = await requireClient().getSentimentIntent(opts.team);
1279
+ console.log(JSON.stringify(res, null, 2));
1280
+ });
1281
+ intel
1282
+ .command("campaign-lift")
1283
+ .description("POST .../subreddit-intelligence/insights/campaign-lift")
1284
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1285
+ .requiredOption("-j, --json <json>", "{ keywords, campaignStartAt, campaignEndAt, windowDays?, minScore?, minComments? }")
1286
+ .action(async (opts) => {
1287
+ const body = JSON.parse(opts.json);
1288
+ const res = await requireClient().getCampaignLift(opts.team, body);
1289
+ console.log(JSON.stringify(res, null, 2));
1290
+ });
1291
+ intel
1292
+ .command("snapshot-latest")
1293
+ .description("GET .../subreddit-intelligence/insights/snapshot-latest")
1294
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1295
+ .action(async (opts) => {
1296
+ const res = await requireClient().getLatestInsightSnapshot(opts.team);
1297
+ console.log(JSON.stringify(res, null, 2));
1298
+ });
1299
+ intel
1300
+ .command("snapshot-save")
1301
+ .description("POST .../subreddit-intelligence/insights/snapshot")
1302
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1303
+ .requiredOption("-j, --json <json>", "{ payload: { ... } }")
1304
+ .action(async (opts) => {
1305
+ const body = JSON.parse(opts.json);
1306
+ const res = await requireClient().saveInsightSnapshot(opts.team, body);
1307
+ console.log(JSON.stringify(res, null, 2));
1308
+ });
906
1309
  // --- users ---
907
1310
  const usersCmd = program.command("users").description("团队成员管理");
908
1311
  usersCmd
@@ -910,8 +1313,28 @@ usersCmd
910
1313
  .description("GET /users")
911
1314
  .requiredOption("-t, --team <teamId>", "Team UUID")
912
1315
  .action(async (opts) => {
913
- const res = await requireClient().listUsers(opts.team);
914
- console.log(JSON.stringify(res, null, 2));
1316
+ await withPermissionGate({
1317
+ command: "users list",
1318
+ resource: "systemMember",
1319
+ action: "list",
1320
+ teamId: opts.team,
1321
+ }, async () => {
1322
+ const res = await requireClient().listUsers(opts.team);
1323
+ console.log(JSON.stringify(res, null, 2));
1324
+ });
1325
+ });
1326
+ usersCmd
1327
+ .command("system-list")
1328
+ .description("GET /v1/system/users")
1329
+ .action(async () => {
1330
+ await withPermissionGate({
1331
+ command: "users system-list",
1332
+ resource: "systemMember",
1333
+ action: "list",
1334
+ }, async () => {
1335
+ const res = await requireClient().listSystemUsers();
1336
+ console.log(JSON.stringify(res, null, 2));
1337
+ });
915
1338
  });
916
1339
  usersCmd
917
1340
  .command("create")
@@ -919,9 +1342,40 @@ usersCmd
919
1342
  .requiredOption("-t, --team <teamId>", "Team UUID")
920
1343
  .requiredOption("-j, --json <json>", "{ email, password, role? }")
921
1344
  .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));
1345
+ await withPermissionGate({
1346
+ command: "users create",
1347
+ resource: "systemMember",
1348
+ action: "create",
1349
+ teamId: opts.team,
1350
+ }, async () => {
1351
+ const body = JSON.parse(opts.json);
1352
+ const res = await requireClient().createUser(opts.team, body);
1353
+ console.log(JSON.stringify(res, null, 2));
1354
+ });
1355
+ });
1356
+ usersCmd
1357
+ .command("system-create")
1358
+ .description("POST /v1/system/users")
1359
+ .requiredOption("-j, --json <json>", "{ email, name, password?, role? }")
1360
+ .option("--dry-run", "预览", false)
1361
+ .option("--apply", "执行写入", false)
1362
+ .action(async (opts) => {
1363
+ await withPermissionGate({
1364
+ command: "users system-create",
1365
+ resource: "systemMember",
1366
+ action: "create",
1367
+ }, async () => {
1368
+ const body = JSON.parse(opts.json);
1369
+ if (!assertApplyOrDryRun(opts, {
1370
+ command: "users system-create",
1371
+ danger: "write",
1372
+ summary: { email: body.email, name: body.name },
1373
+ })) {
1374
+ return;
1375
+ }
1376
+ const res = await requireClient().createSystemUser(body);
1377
+ console.log(JSON.stringify(res, null, 2));
1378
+ });
925
1379
  });
926
1380
  // --- brand-members ---
927
1381
  const brandMembersCmd = program.command("brand-members").description("品牌成员");
@@ -930,17 +1384,63 @@ brandMembersCmd
930
1384
  .description("GET /system/brand-members (preferred)")
931
1385
  .option("--user <uuid>", "userId 过滤")
932
1386
  .action(async (opts) => {
933
- const res = await requireClient().listSystemBrandMembers({ userId: opts.user });
934
- console.log(JSON.stringify(res, null, 2));
1387
+ await withPermissionGate({
1388
+ command: "brand-members system-list",
1389
+ resource: "systemBrand",
1390
+ action: "list",
1391
+ }, async () => {
1392
+ const res = await requireClient().listSystemBrandMembers({
1393
+ userId: opts.user,
1394
+ });
1395
+ console.log(JSON.stringify(res, null, 2));
1396
+ });
935
1397
  });
936
1398
  brandMembersCmd
937
1399
  .command("system-create")
938
1400
  .description("POST /system/brand-members (preferred)")
939
1401
  .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));
1402
+ .option("--dry-run", "预览", false)
1403
+ .option("--apply", "执行写入", false)
1404
+ .action(async (opts) => {
1405
+ await withPermissionGate({
1406
+ command: "brand-members system-create",
1407
+ resource: "brandMember",
1408
+ action: "create",
1409
+ }, async () => {
1410
+ const body = JSON.parse(opts.json);
1411
+ if (!assertApplyOrDryRun(opts, {
1412
+ command: "brand-members system-create",
1413
+ danger: "write",
1414
+ summary: body,
1415
+ })) {
1416
+ return;
1417
+ }
1418
+ const res = await requireClient().createSystemBrandMember(body);
1419
+ console.log(JSON.stringify(res, null, 2));
1420
+ });
1421
+ });
1422
+ brandMembersCmd
1423
+ .command("system-remove")
1424
+ .description("DELETE /system/brand-members/:memberId")
1425
+ .requiredOption("--id <memberId>", "BrandMember UUID")
1426
+ .option("--dry-run", "预览", false)
1427
+ .option("--apply", "执行写入", false)
1428
+ .action(async (opts) => {
1429
+ await withPermissionGate({
1430
+ command: "brand-members system-remove",
1431
+ resource: "brandMember",
1432
+ action: "delete",
1433
+ }, async () => {
1434
+ if (!assertApplyOrDryRun(opts, {
1435
+ command: "brand-members system-remove",
1436
+ danger: "delete",
1437
+ summary: { memberId: opts.id },
1438
+ })) {
1439
+ return;
1440
+ }
1441
+ await requireClient().deleteSystemBrandMember(opts.id);
1442
+ console.log("OK");
1443
+ });
944
1444
  });
945
1445
  brandMembersCmd
946
1446
  .command("list")
@@ -948,18 +1448,43 @@ brandMembersCmd
948
1448
  .requiredOption("-t, --team <teamId>", "Team UUID")
949
1449
  .option("--user <uuid>", "userId 过滤")
950
1450
  .action(async (opts) => {
951
- const res = await requireClient().listBrandMembers(opts.team, { userId: opts.user });
952
- console.log(JSON.stringify(res, null, 2));
1451
+ await withPermissionGate({
1452
+ command: "brand-members list",
1453
+ resource: "brandMember",
1454
+ action: "list",
1455
+ teamId: opts.team,
1456
+ }, async () => {
1457
+ const res = await requireClient().listBrandMembers(opts.team, {
1458
+ userId: opts.user,
1459
+ });
1460
+ console.log(JSON.stringify(res, null, 2));
1461
+ });
953
1462
  });
954
1463
  brandMembersCmd
955
1464
  .command("create")
956
1465
  .description("POST /brand-members")
957
1466
  .requiredOption("-t, --team <teamId>", "Team UUID")
958
1467
  .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));
1468
+ .option("--dry-run", "预览", false)
1469
+ .option("--apply", "执行写入", false)
1470
+ .action(async (opts) => {
1471
+ await withPermissionGate({
1472
+ command: "brand-members create",
1473
+ resource: "brandMember",
1474
+ action: "create",
1475
+ teamId: opts.team,
1476
+ }, async () => {
1477
+ const body = JSON.parse(opts.json);
1478
+ if (!assertApplyOrDryRun(opts, {
1479
+ command: "brand-members create",
1480
+ danger: "write",
1481
+ summary: { teamId: opts.team, ...body },
1482
+ })) {
1483
+ return;
1484
+ }
1485
+ const res = await requireClient().createBrandMember(opts.team, body);
1486
+ console.log(JSON.stringify(res, null, 2));
1487
+ });
963
1488
  });
964
1489
  // --- notification-channels ---
965
1490
  const notifChannels = program
@@ -1023,56 +1548,147 @@ apiKeys
1023
1548
  .requiredOption("-t, --team <teamId>", "Team UUID")
1024
1549
  .option("-n, --limit <n>", "limit", "50")
1025
1550
  .action(async (opts) => {
1026
- const res = await requireClient().listApiKeys(opts.team, { limit: Number(opts.limit) });
1027
- console.log(JSON.stringify(res, null, 2));
1551
+ await withPermissionGate({
1552
+ command: "api-keys list",
1553
+ resource: "apiKey",
1554
+ action: "list",
1555
+ teamId: opts.team,
1556
+ }, async () => {
1557
+ const res = await requireClient().listApiKeys(opts.team, {
1558
+ limit: Number(opts.limit),
1559
+ });
1560
+ console.log(JSON.stringify(res, null, 2));
1561
+ });
1028
1562
  });
1029
1563
  apiKeys
1030
1564
  .command("create")
1031
1565
  .description("POST /api-keys")
1032
1566
  .requiredOption("-t, --team <teamId>", "Team UUID")
1033
1567
  .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));
1568
+ .option("--dry-run", "预览", false)
1569
+ .option("--apply", "执行写入", false)
1570
+ .action(async (opts) => {
1571
+ await withPermissionGate({
1572
+ command: "api-keys create",
1573
+ resource: "apiKey",
1574
+ action: "create",
1575
+ teamId: opts.team,
1576
+ }, async () => {
1577
+ const body = JSON.parse(opts.json);
1578
+ if (!assertApplyOrDryRun(opts, {
1579
+ command: "api-keys create",
1580
+ danger: "secret",
1581
+ summary: { teamId: opts.team, ...body },
1582
+ })) {
1583
+ return;
1584
+ }
1585
+ const res = await requireClient().createApiKey(opts.team, body);
1586
+ console.log(JSON.stringify(res, null, 2));
1587
+ });
1038
1588
  });
1039
1589
  apiKeys
1040
1590
  .command("delete")
1041
1591
  .description("DELETE /api-keys/:id")
1042
1592
  .requiredOption("-t, --team <teamId>", "Team UUID")
1043
1593
  .requiredOption("--key <uuid>", "API Key UUID")
1044
- .action(async (opts) => {
1045
- await requireClient().deleteApiKey(opts.team, opts.key);
1046
- console.log("OK");
1594
+ .option("--dry-run", "预览", false)
1595
+ .option("--apply", "执行写入", false)
1596
+ .action(async (opts) => {
1597
+ await withPermissionGate({
1598
+ command: "api-keys delete",
1599
+ resource: "apiKey",
1600
+ action: "delete",
1601
+ teamId: opts.team,
1602
+ }, async () => {
1603
+ if (!assertApplyOrDryRun(opts, {
1604
+ command: "api-keys delete",
1605
+ danger: "delete",
1606
+ summary: { teamId: opts.team, keyId: opts.key },
1607
+ })) {
1608
+ return;
1609
+ }
1610
+ await requireClient().deleteApiKey(opts.team, opts.key);
1611
+ console.log("OK");
1612
+ });
1047
1613
  });
1048
1614
  apiKeys
1049
1615
  .command("rotate")
1050
1616
  .description("POST /api-keys/:id/rotate")
1051
1617
  .requiredOption("-t, --team <teamId>", "Team UUID")
1052
1618
  .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));
1619
+ .option("--dry-run", "预览", false)
1620
+ .option("--apply", "执行写入", false)
1621
+ .action(async (opts) => {
1622
+ await withPermissionGate({
1623
+ command: "api-keys rotate",
1624
+ resource: "apiKey",
1625
+ action: "update",
1626
+ teamId: opts.team,
1627
+ }, async () => {
1628
+ if (!assertApplyOrDryRun(opts, {
1629
+ command: "api-keys rotate",
1630
+ danger: "secret",
1631
+ summary: { teamId: opts.team, keyId: opts.key },
1632
+ })) {
1633
+ return;
1634
+ }
1635
+ const res = await requireClient().rotateApiKey(opts.team, opts.key);
1636
+ console.log(JSON.stringify(res, null, 2));
1637
+ });
1056
1638
  });
1057
1639
  apiKeys
1058
1640
  .command("batch-revoke")
1059
1641
  .description("POST /api-keys/batch/revoke")
1060
1642
  .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));
1643
+ .requiredOption("-j, --json <json>", '{ "apiKeyIds": ["uuid1","uuid2"] }')
1644
+ .option("--dry-run", "预览", false)
1645
+ .option("--apply", "执行写入", false)
1646
+ .action(async (opts) => {
1647
+ await withPermissionGate({
1648
+ command: "api-keys batch-revoke",
1649
+ resource: "apiKey",
1650
+ action: "delete",
1651
+ teamId: opts.team,
1652
+ }, async () => {
1653
+ const body = JSON.parse(opts.json);
1654
+ const apiKeyIds = body.apiKeyIds ?? body.ids ?? [];
1655
+ if (!assertApplyOrDryRun(opts, {
1656
+ command: "api-keys batch-revoke",
1657
+ danger: "delete",
1658
+ summary: { teamId: opts.team, apiKeyIds },
1659
+ })) {
1660
+ return;
1661
+ }
1662
+ const res = await requireClient().batchRevokeApiKeys(opts.team, apiKeyIds);
1663
+ console.log(JSON.stringify(res, null, 2));
1664
+ });
1066
1665
  });
1067
1666
  apiKeys
1068
1667
  .command("batch-rotate")
1069
1668
  .description("POST /api-keys/batch/rotate")
1070
1669
  .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));
1670
+ .requiredOption("-j, --json <json>", '{ "apiKeyIds": ["uuid1","uuid2"] }')
1671
+ .option("--dry-run", "预览", false)
1672
+ .option("--apply", "执行写入", false)
1673
+ .action(async (opts) => {
1674
+ await withPermissionGate({
1675
+ command: "api-keys batch-rotate",
1676
+ resource: "apiKey",
1677
+ action: "update",
1678
+ teamId: opts.team,
1679
+ }, async () => {
1680
+ const body = JSON.parse(opts.json);
1681
+ const apiKeyIds = body.apiKeyIds ?? body.ids ?? [];
1682
+ if (!assertApplyOrDryRun(opts, {
1683
+ command: "api-keys batch-rotate",
1684
+ danger: "secret",
1685
+ summary: { teamId: opts.team, apiKeyIds },
1686
+ })) {
1687
+ return;
1688
+ }
1689
+ const res = await requireClient().batchRotateApiKeys(opts.team, apiKeyIds);
1690
+ console.log(JSON.stringify(res, null, 2));
1691
+ });
1076
1692
  });
1077
1693
  // ═══════════════════════════════════════════════════════════════════════════
1078
1694
  // Group C: scattered endpoint additions
@@ -1127,9 +1743,16 @@ program
1127
1743
  .requiredOption("-t, --team <teamId>", "Team UUID")
1128
1744
  .requiredOption("-j, --json <json>", "权限矩阵更新 body")
1129
1745
  .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));
1746
+ await withPermissionGate({
1747
+ command: "permissions-update",
1748
+ resource: "permissionMatrix",
1749
+ action: "update",
1750
+ teamId: opts.team,
1751
+ }, async () => {
1752
+ const body = JSON.parse(opts.json);
1753
+ const res = await requireClient().updatePermissionsMatrix(opts.team, body);
1754
+ console.log(JSON.stringify(res, null, 2));
1755
+ });
1133
1756
  });
1134
1757
  graph
1135
1758
  .command("drilldown")
@@ -1144,6 +1767,114 @@ graph
1144
1767
  });
1145
1768
  console.log(JSON.stringify(res, null, 2));
1146
1769
  });
1770
+ graph
1771
+ .command("v2-ego")
1772
+ .description("GET /graph/v2/ego")
1773
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1774
+ .option("--node-id <uuid>", "graph nodeId")
1775
+ .option("--node-ref <id>", "nodeRefId (e.g. account UUID)")
1776
+ .option("--node-type <type>", "account|team|campaign")
1777
+ .option("--relation-type <type>", "relationType filter")
1778
+ .option("--edge-kind <kind>", "edgeKind filter")
1779
+ .option("-n, --limit <n>", "limit", "100")
1780
+ .action(async (opts) => {
1781
+ const res = await requireClient().getGraphV2Ego(opts.team, {
1782
+ nodeId: opts.nodeId,
1783
+ nodeRefId: opts.nodeRef,
1784
+ nodeType: opts.nodeType,
1785
+ relationType: opts.relationType,
1786
+ edgeKind: opts.edgeKind,
1787
+ limit: Number(opts.limit),
1788
+ });
1789
+ console.log(JSON.stringify(res, null, 2));
1790
+ });
1791
+ graph
1792
+ .command("v2-timeline")
1793
+ .description("GET /graph/v2/timeline")
1794
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1795
+ .option("--node-id <uuid>", "graph nodeId")
1796
+ .option("--node-ref <id>", "nodeRefId")
1797
+ .option("--node-type <type>", "account|team|campaign")
1798
+ .option("--from <iso>", "from ISO time")
1799
+ .option("--to <iso>", "to ISO time")
1800
+ .option("-n, --limit <n>", "limit", "100")
1801
+ .action(async (opts) => {
1802
+ const res = await requireClient().getGraphV2Timeline(opts.team, {
1803
+ nodeId: opts.nodeId,
1804
+ nodeRefId: opts.nodeRef,
1805
+ nodeType: opts.nodeType,
1806
+ from: opts.from,
1807
+ to: opts.to,
1808
+ limit: Number(opts.limit),
1809
+ });
1810
+ console.log(JSON.stringify(res, null, 2));
1811
+ });
1812
+ graph
1813
+ .command("v2-explain")
1814
+ .description("GET /graph/v2/explain/:edgeId")
1815
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1816
+ .requiredOption("--edge-id <id>", "edge UUID")
1817
+ .action(async (opts) => {
1818
+ const res = await requireClient().getGraphV2Explain(opts.team, opts.edgeId);
1819
+ console.log(JSON.stringify(res, null, 2));
1820
+ });
1821
+ graph
1822
+ .command("v2-path")
1823
+ .description("GET /graph/v2/path")
1824
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1825
+ .option("--source-id <uuid>", "source nodeId")
1826
+ .option("--source-ref <id>", "source nodeRefId")
1827
+ .option("--source-type <type>", "source nodeType")
1828
+ .option("--target-id <uuid>", "target nodeId")
1829
+ .option("--target-ref <id>", "target nodeRefId")
1830
+ .option("--target-type <type>", "target nodeType")
1831
+ .option("--max-depth <n>", "maxDepth", "4")
1832
+ .option("-n, --limit <n>", "limit", "50")
1833
+ .action(async (opts) => {
1834
+ const res = await requireClient().getGraphV2Path(opts.team, {
1835
+ sourceNodeId: opts.sourceId,
1836
+ sourceNodeRefId: opts.sourceRef,
1837
+ sourceNodeType: opts.sourceType,
1838
+ targetNodeId: opts.targetId,
1839
+ targetNodeRefId: opts.targetRef,
1840
+ targetNodeType: opts.targetType,
1841
+ maxDepth: Number(opts.maxDepth),
1842
+ limit: Number(opts.limit),
1843
+ });
1844
+ console.log(JSON.stringify(res, null, 2));
1845
+ });
1846
+ graph
1847
+ .command("v2-clusters")
1848
+ .description("GET /graph/v2/clusters")
1849
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1850
+ .option("--scope-ref <id>", "scopeNodeRefId")
1851
+ .option("--scope-type <type>", "scopeNodeType", "account")
1852
+ .option("--min-risk <n>", "minRiskScore", "60")
1853
+ .option("-n, --limit <n>", "limit", "100")
1854
+ .action(async (opts) => {
1855
+ const res = await requireClient().getGraphV2Clusters(opts.team, {
1856
+ scopeNodeRefId: opts.scopeRef,
1857
+ scopeNodeType: opts.scopeType,
1858
+ minRiskScore: Number(opts.minRisk),
1859
+ limit: Number(opts.limit),
1860
+ });
1861
+ console.log(JSON.stringify(res, null, 2));
1862
+ });
1863
+ graph
1864
+ .command("v2-impact")
1865
+ .description("GET /graph/v2/impact")
1866
+ .requiredOption("-t, --team <teamId>", "Team UUID")
1867
+ .option("--campaign-ref <id>", "campaignNodeRefId")
1868
+ .option("--min-reuse <n>", "minReuseCount", "2")
1869
+ .option("-n, --limit <n>", "limit", "100")
1870
+ .action(async (opts) => {
1871
+ const res = await requireClient().getGraphV2Impact(opts.team, {
1872
+ campaignNodeRefId: opts.campaignRef,
1873
+ minReuseCount: Number(opts.minReuse),
1874
+ limit: Number(opts.limit),
1875
+ });
1876
+ console.log(JSON.stringify(res, null, 2));
1877
+ });
1147
1878
  // ═══════════════════════════════════════════════════════════════════════════
1148
1879
  // Group D: add filter params to existing list commands
1149
1880
  // ═══════════════════════════════════════════════════════════════════════════
@@ -1380,9 +2111,25 @@ brandMembersCmd
1380
2111
  .description("DELETE /brand-members/:memberId")
1381
2112
  .requiredOption("-t, --team <teamId>", "Team UUID")
1382
2113
  .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));
2114
+ .option("--dry-run", "预览", false)
2115
+ .option("--apply", "执行写入", false)
2116
+ .action(async (opts) => {
2117
+ await withPermissionGate({
2118
+ command: "brand-members remove",
2119
+ resource: "brandMember",
2120
+ action: "delete",
2121
+ teamId: opts.team,
2122
+ }, async () => {
2123
+ if (!assertApplyOrDryRun(opts, {
2124
+ command: "brand-members remove",
2125
+ danger: "delete",
2126
+ summary: { teamId: opts.team, memberId: opts.member },
2127
+ })) {
2128
+ return;
2129
+ }
2130
+ const res = await requireClient().deleteBrandMember(opts.team, opts.member);
2131
+ console.log(JSON.stringify(res, null, 2));
2132
+ });
1386
2133
  });
1387
2134
  // Only parse when run directly (not when imported for testing)
1388
2135
  if (!process.env["VITEST"]) {