gangtise-openapi-cli 0.14.2 → 0.14.3

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/src/cli.js CHANGED
@@ -17,6 +17,20 @@ async function createClient() {
17
17
  const { GangtiseClient } = await import("./core/client.js");
18
18
  return new GangtiseClient(loadConfig());
19
19
  }
20
+ /**
21
+ * Acquire a client, run `produce` to fetch data, and render it through the
22
+ * shared pipeline. Collapses the `createClient()` + `printData(await client.call(...),
23
+ * parseOutputFormat(options.format), options.output)` boilerplate that every
24
+ * query command repeated.
25
+ */
26
+ async function emit(options, produce, cache) {
27
+ const client = await createClient();
28
+ await printData(await produce(client), parseOutputFormat(options.format), options.output, cache);
29
+ }
30
+ /** Acquire a client and run an arbitrary action (downloads, polling, custom shaping). */
31
+ async function withClient(fn) {
32
+ await fn(await createClient());
33
+ }
20
34
  /**
21
35
  * Run a download. If `output` is set we already know the destination, so the
22
36
  * client streams the body straight to disk (no in-memory Uint8Array copy);
@@ -57,10 +71,7 @@ program
57
71
  .description("Authentication commands")
58
72
  .addCommand(new Command("login")
59
73
  .option("--format <format>", "Output format", "json")
60
- .action(async (options) => {
61
- const client = await createClient();
62
- await printData(await client.login(), parseOutputFormat(options.format));
63
- }))
74
+ .action((options) => emit(options, (client) => client.login())))
64
75
  .addCommand(new Command("status")
65
76
  .option("--format <format>", "Output format", "json")
66
77
  .action(async (options) => {
@@ -70,38 +81,14 @@ program
70
81
  }));
71
82
  const lookup = new Command("lookup").description("Lookup helper APIs");
72
83
  lookup
73
- .addCommand(new Command("research-area").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
74
- const client = await createClient();
75
- await printData(await client.call("lookup.research-areas.list"), parseOutputFormat(options.format));
76
- })))
77
- .addCommand(new Command("broker-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
78
- const client = await createClient();
79
- await printData(await client.call("lookup.broker-orgs.list"), parseOutputFormat(options.format));
80
- })))
81
- .addCommand(new Command("meeting-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
82
- const client = await createClient();
83
- await printData(await client.call("lookup.meeting-orgs.list"), parseOutputFormat(options.format));
84
- })))
85
- .addCommand(new Command("industry").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
86
- const client = await createClient();
87
- await printData(await client.call("lookup.industries.list"), parseOutputFormat(options.format));
88
- })))
89
- .addCommand(new Command("region").description("Foreign report region codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
90
- const client = await createClient();
91
- await printData(await client.call("lookup.regions.list"), parseOutputFormat(options.format));
92
- })))
93
- .addCommand(new Command("announcement-category").description("Announcement category codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
94
- const client = await createClient();
95
- await printData(await client.call("lookup.announcement-categories.list"), parseOutputFormat(options.format));
96
- })))
97
- .addCommand(new Command("industry-code").description("Shenwan industry codes for security-clue --gts-code").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
98
- const client = await createClient();
99
- await printData(await client.call("lookup.industry-codes.list"), parseOutputFormat(options.format));
100
- })))
101
- .addCommand(new Command("theme-id").description("Theme IDs for theme-tracking --theme-id").addCommand(new Command("list").option("--format <format>", "Output format", "table").action(async (options) => {
102
- const client = await createClient();
103
- await printData(await client.call("lookup.theme-ids.list"), parseOutputFormat(options.format));
104
- })));
84
+ .addCommand(new Command("research-area").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.research-areas.list")))))
85
+ .addCommand(new Command("broker-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.broker-orgs.list")))))
86
+ .addCommand(new Command("meeting-org").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.meeting-orgs.list")))))
87
+ .addCommand(new Command("industry").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.industries.list")))))
88
+ .addCommand(new Command("region").description("Foreign report region codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.regions.list")))))
89
+ .addCommand(new Command("announcement-category").description("Announcement category codes").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.announcement-categories.list")))))
90
+ .addCommand(new Command("industry-code").description("Shenwan industry codes for security-clue --gts-code").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.industry-codes.list")))))
91
+ .addCommand(new Command("theme-id").description("Theme IDs for theme-tracking --theme-id").addCommand(new Command("list").option("--format <format>", "Output format", "table").action((options) => emit(options, (client) => client.call("lookup.theme-ids.list")))));
105
92
  program.addCommand(lookup);
106
93
  const insight = new Command("insight").description("Insight APIs");
107
94
  const opinion = new Command("opinion");
@@ -116,26 +103,19 @@ const announcement = new Command("announcement");
116
103
  const announcementHk = new Command("announcement-hk");
117
104
  const foreignOpinion = new Command("foreign-opinion");
118
105
  const independentOpinion = new Command("independent-opinion");
119
- addTimeFilters(opinion.command("list").option("--rank-type <number>", "Rank type", "1").option("--research-area <id>", "Research area ID", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
120
- const client = await createClient();
121
- await printData(await client.call("insight.opinion.list", {
122
- from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
123
- rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
124
- securityList: maybeArray(options.security), brokerList: maybeArray(options.broker), industryList: maybeArray(options.industry), conceptList: maybeArray(options.concept),
125
- llmTagList: maybeArray(options.llmTag), sourceList: maybeArray(options.source),
126
- }), parseOutputFormat(options.format), options.output);
127
- });
128
- addTimeFilters(summary.command("list").option("--search-type <number>", "Search type", "1").option("--rank-type <number>", "Rank type", "1").option("--source <number>", "Source type", collectNumberList, []).option("--research-area <id>", "Research area", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
129
- const client = await createClient();
130
- await printData(await client.call("insight.summary.list", {
131
- from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
132
- searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, sourceList: options.source.length ? options.source : undefined,
133
- researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution),
134
- categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
135
- }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.summary.list", idField: "summaryId" });
136
- });
137
- summary.command("download").requiredOption("--summary-id <id>").option("--file-type <number>", "File type: 1=original(default) 2=HTML; only affects meeting platform summaries").option("--output <path>").action(async (options) => {
138
- const client = await createClient();
106
+ addTimeFilters(opinion.command("list").option("--rank-type <number>", "Rank type", "1").option("--research-area <id>", "Research area ID", collectList, []).option("--chief <id>", "Chief ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--concept <id>", "Concept ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--source <source>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.opinion.list", {
107
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
108
+ rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), chiefList: maybeArray(options.chief),
109
+ securityList: maybeArray(options.security), brokerList: maybeArray(options.broker), industryList: maybeArray(options.industry), conceptList: maybeArray(options.concept),
110
+ llmTagList: maybeArray(options.llmTag), sourceList: maybeArray(options.source),
111
+ })));
112
+ addTimeFilters(summary.command("list").option("--search-type <number>", "Search type", "1").option("--rank-type <number>", "Rank type", "1").option("--source <number>", "Source type", collectNumberList, []).option("--research-area <id>", "Research area", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.summary.list", {
113
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime,
114
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword, sourceList: options.source.length ? options.source : undefined,
115
+ researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution),
116
+ categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
117
+ }), { endpointKey: "insight.summary.list", idField: "summaryId" }));
118
+ summary.command("download").requiredOption("--summary-id <id>").option("--file-type <number>", "File type: 1=original(default) 2=HTML; only affects meeting platform summaries").option("--output <path>").action((options) => withClient(async (client) => {
139
119
  const qp = { summaryId: options.summaryId };
140
120
  if (options.fileType)
141
121
  qp.fileType = parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 });
@@ -144,124 +124,98 @@ summary.command("download").requiredOption("--summary-id <id>").option("--file-t
144
124
  fallbackName: `summary-${options.summaryId}`,
145
125
  resolveOutputPath: (result) => resolveTitle(client, result, "insight.summary.list", "summaryId", options.summaryId),
146
126
  });
147
- });
148
- const addScheduleList = (command, endpointKey) => addTimeFilters(command.command("list").option("--research-area <id>", "Research area", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--broker-type <name>", "Broker type", collectList, []).option("--object <type>", "Object type: company/industry", collectList, []).option("--permission <number>", "Permission", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
149
- const client = await createClient();
150
- await printData(await client.call(endpointKey, {
151
- from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
152
- researchAreaList: maybeArray(options.researchArea), institutionList: maybeArray(options.institution), securityList: maybeArray(options.security),
153
- categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
154
- brokerTypeList: maybeArray(options.brokerType), objectList: maybeArray(options.object), permission: options.permission.length ? options.permission : undefined,
155
- }), parseOutputFormat(options.format), options.output);
156
- });
127
+ }));
128
+ const addScheduleList = (command, endpointKey) => addTimeFilters(command.command("list").option("--research-area <id>", "Research area", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--category <name>", "Category", collectList, []).option("--market <name>", "Market", collectList, []).option("--participant-role <name>", "Participant role", collectList, []).option("--broker-type <name>", "Broker type", collectList, []).option("--object <type>", "Object type: company/industry", collectList, []).option("--permission <number>", "Permission", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call(endpointKey, {
129
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
130
+ researchAreaList: maybeArray(options.researchArea), institutionList: maybeArray(options.institution), securityList: maybeArray(options.security),
131
+ categoryList: maybeArray(options.category), marketList: maybeArray(options.market), participantRoleList: maybeArray(options.participantRole),
132
+ brokerTypeList: maybeArray(options.brokerType), objectList: maybeArray(options.object), permission: options.permission.length ? options.permission : undefined,
133
+ })));
157
134
  addScheduleList(roadshow, "insight.roadshow.list");
158
135
  addScheduleList(siteVisit, "insight.site-visit.list");
159
136
  addScheduleList(strategy, "insight.strategy.list");
160
137
  addScheduleList(forum, "insight.forum.list");
161
- addTimeFilters(research.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--broker <id>", "Broker ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--min-pages <number>", "Min report pages").option("--max-pages <number>", "Max report pages").option("--source <type>", "Source type", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
162
- const client = await createClient();
163
- await printData(await client.call("insight.research.list", {
164
- from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
165
- searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
166
- brokerList: maybeArray(options.broker), securityList: maybeArray(options.security), industryList: maybeArray(options.industry),
167
- categoryList: maybeArray(options.category), llmTagList: maybeArray(options.llmTag), ratingList: maybeArray(options.rating),
168
- ratingChangeList: maybeArray(options.ratingChange), minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }),
169
- maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }), sourceList: maybeArray(options.source),
170
- }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.research.list", idField: "reportId" });
171
- });
172
- research.command("download").requiredOption("--report-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action(async (options) => {
173
- const client = await createClient();
138
+ addTimeFilters(research.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--broker <id>", "Broker ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--min-pages <number>", "Min report pages").option("--max-pages <number>", "Max report pages").option("--source <type>", "Source type", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.research.list", {
139
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
140
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
141
+ brokerList: maybeArray(options.broker), securityList: maybeArray(options.security), industryList: maybeArray(options.industry),
142
+ categoryList: maybeArray(options.category), llmTagList: maybeArray(options.llmTag), ratingList: maybeArray(options.rating),
143
+ ratingChangeList: maybeArray(options.ratingChange), minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }),
144
+ maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }), sourceList: maybeArray(options.source),
145
+ }), { endpointKey: "insight.research.list", idField: "reportId" }));
146
+ research.command("download").requiredOption("--report-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action((options) => withClient(async (client) => {
174
147
  await runDownload(client, "insight.research.download", { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
175
148
  output: options.output,
176
149
  fallbackName: `research-${options.reportId}`,
177
150
  resolveOutputPath: (result) => resolveTitle(client, result, "insight.research.list", "reportId", options.reportId),
178
151
  });
179
- });
180
- addTimeFilters(foreignReport.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code", collectList, []).option("--region <id>", "Region ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--min-pages <number>", "Min report pages").option("--max-pages <number>", "Max report pages").option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
181
- const client = await createClient();
182
- await printData(await client.call("insight.foreign-report.list", {
183
- from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
184
- searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
185
- securityList: maybeArray(options.security), regionList: maybeArray(options.region), categoryList: maybeArray(options.category),
186
- industryList: maybeArray(options.industry), brokerList: maybeArray(options.broker), llmTagList: maybeArray(options.llmTag),
187
- ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
188
- minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }), maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }),
189
- }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.foreign-report.list", idField: "reportId" });
190
- });
191
- foreignReport.command("download").requiredOption("--report-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown 3=CN-PDF 4=CN-Markdown", "1").option("--output <path>").action(async (options) => {
192
- const client = await createClient();
152
+ }));
153
+ addTimeFilters(foreignReport.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code", collectList, []).option("--region <id>", "Region ID", collectList, []).option("--category <name>", "Report category", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--llm-tag <tag>", "Semantic tag", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--min-pages <number>", "Min report pages").option("--max-pages <number>", "Max report pages").option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.foreign-report.list", {
154
+ from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword,
155
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
156
+ securityList: maybeArray(options.security), regionList: maybeArray(options.region), categoryList: maybeArray(options.category),
157
+ industryList: maybeArray(options.industry), brokerList: maybeArray(options.broker), llmTagList: maybeArray(options.llmTag),
158
+ ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
159
+ minReportPages: parseOptionalNumberOption(options.minPages, "--min-pages", { integer: true, min: 0 }), maxReportPages: parseOptionalNumberOption(options.maxPages, "--max-pages", { integer: true, min: 0 }),
160
+ }), { endpointKey: "insight.foreign-report.list", idField: "reportId" }));
161
+ foreignReport.command("download").requiredOption("--report-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown 3=CN-PDF 4=CN-Markdown", "1").option("--output <path>").action((options) => withClient(async (client) => {
193
162
  await runDownload(client, "insight.foreign-report.download", { reportId: options.reportId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
194
163
  output: options.output,
195
164
  fallbackName: `foreign-report-${options.reportId}`,
196
165
  resolveOutputPath: (result) => resolveTitle(client, result, "insight.foreign-report.list", "reportId", options.reportId),
197
166
  });
198
- });
199
- addTimeFilters(announcement.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code", collectList, []).option("--announcement-type <type>", "Announcement type", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
200
- const client = await createClient();
201
- await printData(await client.call("insight.announcement.list", {
202
- from: parseFrom(options.from), size: parseSize(options.size),
203
- startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time"),
204
- searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
205
- securityList: maybeArray(options.security), announcementTypeList: maybeArray(options.announcementType), categoryList: maybeArray(options.category),
206
- }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.announcement.list", idField: "announcementId" });
207
- });
208
- announcement.command("download").requiredOption("--announcement-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action(async (options) => {
209
- const client = await createClient();
167
+ }));
168
+ addTimeFilters(announcement.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code", collectList, []).option("--announcement-type <type>", "Announcement type", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.announcement.list", {
169
+ from: parseFrom(options.from), size: parseSize(options.size),
170
+ startTime: parseTimestamp13(options.startTime, "--start-time"), endTime: parseTimestamp13(options.endTime, "--end-time"),
171
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }), rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }), keyword: options.keyword,
172
+ securityList: maybeArray(options.security), announcementTypeList: maybeArray(options.announcementType), categoryList: maybeArray(options.category),
173
+ }), { endpointKey: "insight.announcement.list", idField: "announcementId" }));
174
+ announcement.command("download").requiredOption("--announcement-id <id>").option("--file-type <number>", "File type: 1=PDF 2=Markdown", "1").option("--output <path>").action((options) => withClient(async (client) => {
210
175
  await runDownload(client, "insight.announcement.download", { announcementId: options.announcementId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
211
176
  output: options.output,
212
177
  fallbackName: `announcement-${options.announcementId}`,
213
178
  resolveOutputPath: (result) => resolveTitle(client, result, "insight.announcement.list", "announcementId", options.announcementId),
214
179
  });
215
- });
216
- addTimeFilters(announcementHk.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. 01913.HK)", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
217
- const client = await createClient();
218
- await printData(await client.call("insight.announcement-hk.list", {
219
- from: parseFrom(options.from), size: parseSize(options.size),
220
- startTime: options.startTime, endTime: options.endTime,
221
- searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }),
222
- rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
223
- keyword: options.keyword,
224
- securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
225
- }), parseOutputFormat(options.format), options.output, { endpointKey: "insight.announcement-hk.list", idField: "announcementId" });
226
- });
227
- announcementHk.command("download").requiredOption("--announcement-id <id>").option("--output <path>").action(async (options) => {
228
- const client = await createClient();
180
+ }));
181
+ addTimeFilters(announcementHk.command("list").option("--search-type <number>", "Search type: 1=title 2=fulltext", "1").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. 01913.HK)", collectList, []).option("--category <id>", "Category ID", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.announcement-hk.list", {
182
+ from: parseFrom(options.from), size: parseSize(options.size),
183
+ startTime: options.startTime, endTime: options.endTime,
184
+ searchType: parseNumberOption(options.searchType, "--search-type", { integer: true, min: 1 }),
185
+ rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
186
+ keyword: options.keyword,
187
+ securityList: maybeArray(options.security), categoryList: maybeArray(options.category),
188
+ }), { endpointKey: "insight.announcement-hk.list", idField: "announcementId" }));
189
+ announcementHk.command("download").requiredOption("--announcement-id <id>").option("--output <path>").action((options) => withClient(async (client) => {
229
190
  await runDownload(client, "insight.announcement-hk.download", { announcementId: options.announcementId }, {
230
191
  output: options.output,
231
192
  fallbackName: `announcement-hk-${options.announcementId}`,
232
193
  resolveOutputPath: (result) => resolveTitle(client, result, "insight.announcement-hk.list", "announcementId", options.announcementId),
233
194
  });
234
- });
235
- addTimeFilters(foreignOpinion.command("list").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
236
- const client = await createClient();
237
- await printData(await client.call("insight.foreign-opinion.list", {
238
- from: parseFrom(options.from), size: parseSize(options.size),
239
- startTime: options.startTime, endTime: options.endTime,
240
- rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
241
- keyword: options.keyword,
242
- regionList: maybeArray(options.region), industryList: maybeArray(options.industry),
243
- securityList: maybeArray(options.security), brokerList: maybeArray(options.broker),
244
- ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
245
- }), parseOutputFormat(options.format), options.output);
246
- });
247
- addTimeFilters(independentOpinion.command("list").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. GSK.N)", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action(async (options) => {
248
- const client = await createClient();
249
- await printData(await client.call("insight.independent-opinion.list", {
250
- from: parseFrom(options.from), size: parseSize(options.size),
251
- startTime: options.startTime, endTime: options.endTime,
252
- rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
253
- keyword: options.keyword,
254
- industryList: maybeArray(options.industry), securityList: maybeArray(options.security),
255
- ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
256
- }), parseOutputFormat(options.format), options.output);
257
- });
258
- independentOpinion.command("download").requiredOption("--independent-opinion-id <id>").requiredOption("--file-type <number>", "File type: 1=original HTML 2=CN-translated HTML").option("--output <path>").action(async (options) => {
259
- const client = await createClient();
195
+ }));
196
+ addTimeFilters(foreignOpinion.command("list").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. UBER.N)", collectList, []).option("--region <code>", "Region code", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--broker <id>", "Broker ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.foreign-opinion.list", {
197
+ from: parseFrom(options.from), size: parseSize(options.size),
198
+ startTime: options.startTime, endTime: options.endTime,
199
+ rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
200
+ keyword: options.keyword,
201
+ regionList: maybeArray(options.region), industryList: maybeArray(options.industry),
202
+ securityList: maybeArray(options.security), brokerList: maybeArray(options.broker),
203
+ ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
204
+ })));
205
+ addTimeFilters(independentOpinion.command("list").option("--rank-type <number>", "Rank type: 1=composite 2=time desc", "1").option("--security <code>", "Security code (e.g. GSK.N)", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--rating <name>", "Rating", collectList, []).option("--rating-change <name>", "Rating change", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>", "Output path")).action((options) => emit(options, (client) => client.call("insight.independent-opinion.list", {
206
+ from: parseFrom(options.from), size: parseSize(options.size),
207
+ startTime: options.startTime, endTime: options.endTime,
208
+ rankType: parseNumberOption(options.rankType, "--rank-type", { integer: true, min: 1 }),
209
+ keyword: options.keyword,
210
+ industryList: maybeArray(options.industry), securityList: maybeArray(options.security),
211
+ ratingList: maybeArray(options.rating), ratingChangeList: maybeArray(options.ratingChange),
212
+ })));
213
+ independentOpinion.command("download").requiredOption("--independent-opinion-id <id>").requiredOption("--file-type <number>", "File type: 1=original HTML 2=CN-translated HTML").option("--output <path>").action((options) => withClient(async (client) => {
260
214
  await runDownload(client, "insight.independent-opinion.download", { independentOpinionId: options.independentOpinionId, fileType: parseNumberOption(options.fileType, "--file-type", { integer: true, min: 1 }) }, {
261
215
  output: options.output,
262
216
  fallbackName: `independent-opinion-${options.independentOpinionId}`,
263
217
  });
264
- });
218
+ }));
265
219
  insight.addCommand(opinion);
266
220
  insight.addCommand(summary);
267
221
  insight.addCommand(roadshow);
@@ -276,36 +230,15 @@ insight.addCommand(foreignOpinion);
276
230
  insight.addCommand(independentOpinion);
277
231
  program.addCommand(insight);
278
232
  const quote = new Command("quote").description("Quote APIs");
279
- quote.command("day-kline").option("--security <code>", "Security code (A-share: .SH/.SZ/.BJ, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
280
- const client = await createClient();
281
- await printData(await callKlineWithSharding(client, "quote.day-kline", buildQuoteKlineBody(options), { shardDays: 1 }), parseOutputFormat(options.format), options.output);
282
- });
283
- quote.command("day-kline-hk").option("--security <code>", "Security code (HK stock: .HK, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
284
- const client = await createClient();
285
- await printData(await callKlineWithSharding(client, "quote.day-kline-hk", buildQuoteKlineBody(options), { shardDays: 2 }), parseOutputFormat(options.format), options.output);
286
- });
287
- quote.command("day-kline-us").option("--security <code>", "Security code (US stock: e.g. AAPL.O, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
288
- const client = await createClient();
289
- await printData(await callKlineWithSharding(client, "quote.day-kline-us", buildQuoteKlineBody(options), { shardDays: 1 }), parseOutputFormat(options.format), options.output);
290
- });
291
- quote.command("index-day-kline").option("--security <code>", "Index code (.SH/.SZ/.BJ, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
292
- const client = await createClient();
293
- await printData(await callKlineWithSharding(client, "quote.index-day-kline", buildQuoteKlineBody(options), { shardDays: 30 }), parseOutputFormat(options.format), options.output);
294
- });
295
- quote.command("minute-kline").option("--security <code>", "Security code (A-share only: .SH/.SZ/.BJ)").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)").option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)").option("--limit <number>", "Max rows per request (default: 5000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
296
- const client = await createClient();
297
- await printData(await client.call("quote.minute-kline", { securityCode: options.security, startTime: options.startTime, endTime: options.endTime, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
298
- });
299
- quote.command("realtime").description("Realtime quote snapshot (A-share / HK / US)").option("--security <code>", "Security code (e.g. 600519.SH / 00700.HK / AAPL.O), or market keyword: aShares / hkStocks / usStocks", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
300
- const client = await createClient();
301
- await printData(await client.call("quote.realtime", { securityList: maybeArray(options.security), fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
302
- });
233
+ quote.command("day-kline").option("--security <code>", "Security code (A-share: .SH/.SZ/.BJ, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => callKlineWithSharding(client, "quote.day-kline", buildQuoteKlineBody(options), { shardDays: 1 })));
234
+ quote.command("day-kline-hk").option("--security <code>", "Security code (HK stock: .HK, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => callKlineWithSharding(client, "quote.day-kline-hk", buildQuoteKlineBody(options), { shardDays: 2 })));
235
+ quote.command("day-kline-us").option("--security <code>", "Security code (US stock: e.g. AAPL.O, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => callKlineWithSharding(client, "quote.day-kline-us", buildQuoteKlineBody(options), { shardDays: 1 })));
236
+ quote.command("index-day-kline").option("--security <code>", "Index code (.SH/.SZ/.BJ, or 'all' for full market)", collectList, []).option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: latest)").option("--limit <number>", "Max rows per request (default: 6000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => callKlineWithSharding(client, "quote.index-day-kline", buildQuoteKlineBody(options), { shardDays: 30 })));
237
+ quote.command("minute-kline").option("--security <code>", "Security code (A-share only: .SH/.SZ/.BJ)").option("--start-time <datetime>", "Start time (yyyy-MM-dd HH:mm:ss)").option("--end-time <datetime>", "End time (yyyy-MM-dd HH:mm:ss)").option("--limit <number>", "Max rows per request (default: 5000, max: 10000)").option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("quote.minute-kline", { securityCode: options.security, startTime: options.startTime, endTime: options.endTime, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) })));
238
+ quote.command("realtime").description("Realtime quote snapshot (A-share / HK / US)").option("--security <code>", "Security code (e.g. 600519.SH / 00700.HK / AAPL.O), or market keyword: aShares / hkStocks / usStocks", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("quote.realtime", { securityList: maybeArray(options.security), fieldList: maybeArray(options.field) })));
303
239
  program.addCommand(quote);
304
240
  const fundamental = new Command("fundamental").description("Fundamental APIs");
305
- const addFinancialReport = (name, endpointKey, periodHelp = "Period") => fundamental.command(name).requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", periodHelp, collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
306
- const client = await createClient();
307
- await printData(await client.call(endpointKey, { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
308
- });
241
+ const addFinancialReport = (name, endpointKey, periodHelp = "Period") => fundamental.command(name).requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", periodHelp, collectList, []).option("--report-type <type>", "Report type", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call(endpointKey, { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined, reportType: options.reportType.length ? options.reportType : undefined, fieldList: maybeArray(options.field) })));
309
242
  addFinancialReport("income-statement", "fundamental.income-statement");
310
243
  addFinancialReport("income-statement-quarterly", "fundamental.income-statement-quarterly", "Period: q1/q2/q3/q4/latest");
311
244
  addFinancialReport("balance-sheet", "fundamental.balance-sheet");
@@ -314,15 +247,11 @@ addFinancialReport("cash-flow-quarterly", "fundamental.cash-flow-quarterly", "Pe
314
247
  addFinancialReport("income-statement-hk", "fundamental.income-statement-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
315
248
  addFinancialReport("balance-sheet-hk", "fundamental.balance-sheet-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
316
249
  addFinancialReport("cash-flow-hk", "fundamental.cash-flow-hk", "Period: q1/h1/q3/h2/nsd/annual/latest");
317
- fundamental.command("main-business").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").addOption(new Option("--breakdown <type>", "Breakdown: product/industry/region").choices(["product", "industry", "region"]).default("product")).option("--period <type>", "Period: interim/annual", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
318
- const client = await createClient();
319
- await printData(await client.call("fundamental.main-business", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, breakdown: options.breakdown, periodList: maybeArray(options.period), fieldList: maybeArray(options.field) }), parseOutputFormat(options.format), options.output);
320
- });
321
- fundamental.command("valuation-analysis").requiredOption("--security-code <code>").addOption(new Option("--indicator <name>", "Indicator").choices(["peTtm", "pbMrq", "peg", "psTtm", "pcfTtm", "em"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--limit <number>").option("--field <field>", "Field", collectList, []).option("--skip-null", "Drop rows where value or percentileRank is null").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
322
- const client = await createClient();
250
+ fundamental.command("main-business").requiredOption("--security-code <code>").option("--start-date <date>").option("--end-date <date>").addOption(new Option("--breakdown <type>", "Breakdown: product/industry/region").choices(["product", "industry", "region"]).default("product")).option("--period <type>", "Period: interim/annual", collectList, []).option("--field <field>", "Field", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("fundamental.main-business", { securityCode: options.securityCode, startDate: options.startDate, endDate: options.endDate, breakdown: options.breakdown, periodList: maybeArray(options.period), fieldList: maybeArray(options.field) })));
251
+ fundamental.command("valuation-analysis").requiredOption("--security-code <code>").addOption(new Option("--indicator <name>", "Indicator").choices(["peTtm", "pbMrq", "peg", "psTtm", "pcfTtm", "em"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--limit <number>").option("--field <field>", "Field", collectList, []).option("--skip-null", "Drop rows where value or percentileRank is null").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
323
252
  let data = await client.call("fundamental.valuation-analysis", { securityCode: options.securityCode, indicator: options.indicator, startDate: options.startDate, endDate: options.endDate, limit: parseOptionalNumberOption(options.limit, "--limit", { integer: true, min: 1 }), fieldList: maybeArray(options.field) });
324
253
  if (options.skipNull) {
325
- const normalized = await normalizeRows(data);
254
+ const normalized = normalizeRows(data);
326
255
  if (normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
327
256
  const rec = normalized;
328
257
  if (Array.isArray(rec.list)) {
@@ -337,48 +266,27 @@ fundamental.command("valuation-analysis").requiredOption("--security-code <code>
337
266
  }
338
267
  }
339
268
  await printData(data, parseOutputFormat(options.format), options.output);
340
- });
341
- fundamental.command("top-holders").requiredOption("--security-code <code>").addOption(new Option("--holder-type <type>", "Holder type: top10/top10Float").choices(["top10", "top10Float"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period: q1/interim/q3/annual/latest", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
342
- const client = await createClient();
343
- await printData(await client.call("fundamental.top-holders", { securityCode: options.securityCode, holderType: options.holderType, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined }), parseOutputFormat(options.format), options.output);
344
- });
345
- fundamental.command("earning-forecast").requiredOption("--security-code <code>").option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: today)").option("--consensus <name>", "Consensus indicator: netIncome/netIncomeYoy/eps/pe/bps/pb/peg/roe/ps", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
346
- const client = await createClient();
269
+ }));
270
+ fundamental.command("top-holders").requiredOption("--security-code <code>").addOption(new Option("--holder-type <type>", "Holder type: top10/top10Float").choices(["top10", "top10Float"]).makeOptionMandatory()).option("--start-date <date>").option("--end-date <date>").option("--fiscal-year <year>", "Fiscal year", collectList, []).option("--period <period>", "Period: q1/interim/q3/annual/latest", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("fundamental.top-holders", { securityCode: options.securityCode, holderType: options.holderType, startDate: options.startDate, endDate: options.endDate, fiscalYear: maybeArray(options.fiscalYear), period: options.period.length ? options.period : undefined })));
271
+ fundamental.command("earning-forecast").requiredOption("--security-code <code>").option("--start-date <date>", "Start date (default: 1 year before end-date)").option("--end-date <date>", "End date (default: today)").option("--consensus <name>", "Consensus indicator: netIncome/netIncomeYoy/eps/pe/bps/pb/peg/roe/ps", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => {
347
272
  const endDate = options.endDate ?? new Date().toISOString().slice(0, 10);
348
273
  const startDate = options.startDate ?? new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
349
- await printData(await client.call("fundamental.earning-forecast", { securityCode: options.securityCode, startDate, endDate, consensusList: maybeArray(options.consensus) }), parseOutputFormat(options.format), options.output);
350
- });
274
+ return client.call("fundamental.earning-forecast", { securityCode: options.securityCode, startDate, endDate, consensusList: maybeArray(options.consensus) });
275
+ }));
351
276
  program.addCommand(fundamental);
352
277
  const ai = new Command("ai").description("AI APIs");
353
- ai.command("knowledge-batch").requiredOption("--query <text>", "Query", collectList, []).option("--top <number>", "Top", "10").option("--resource-type <number>", "Resource type", collectNumberList, []).option("--knowledge-name <name>", "Knowledge name", collectList, []).option("--start-time <ms>").option("--end-time <ms>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
354
- const client = await createClient();
355
- await printData(await client.call("ai.knowledge-batch", { queries: options.query, top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }), resourceTypes: options.resourceType.length ? options.resourceType : undefined, knowledgeNames: maybeArray(options.knowledgeName), startTime: parseOptionalNumberOption(options.startTime, "--start-time", { integer: true, min: 0 }), endTime: parseOptionalNumberOption(options.endTime, "--end-time", { integer: true, min: 0 }) }), parseOutputFormat(options.format), options.output);
356
- });
357
- ai.command("knowledge-resource-download").requiredOption("--resource-type <number>").requiredOption("--source-id <id>").option("--output <path>").action(async (options) => {
358
- const client = await createClient();
278
+ ai.command("knowledge-batch").requiredOption("--query <text>", "Query", collectList, []).option("--top <number>", "Top", "10").option("--resource-type <number>", "Resource type", collectNumberList, []).option("--knowledge-name <name>", "Knowledge name", collectList, []).option("--start-time <ms>").option("--end-time <ms>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.knowledge-batch", { queries: options.query, top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }), resourceTypes: options.resourceType.length ? options.resourceType : undefined, knowledgeNames: maybeArray(options.knowledgeName), startTime: parseOptionalNumberOption(options.startTime, "--start-time", { integer: true, min: 0 }), endTime: parseOptionalNumberOption(options.endTime, "--end-time", { integer: true, min: 0 }) })));
279
+ ai.command("knowledge-resource-download").requiredOption("--resource-type <number>").requiredOption("--source-id <id>").option("--output <path>").action((options) => withClient(async (client) => {
359
280
  await runDownload(client, "ai.knowledge-resource.download", { resourceType: parseNumberOption(options.resourceType, "--resource-type", { integer: true, min: 0 }), sourceId: options.sourceId }, {
360
281
  output: options.output,
361
282
  fallbackName: `resource-${options.sourceId}`,
362
283
  });
363
- });
364
- ai.command("security-clue").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").requiredOption("--start-time <datetime>").requiredOption("--end-time <datetime>").addOption(new Option("--query-mode <mode>").choices(["bySecurity", "byIndustry"]).makeOptionMandatory()).option("--gts-code <code>", "GTS code", collectList, []).option("--source <name>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
365
- const client = await createClient();
366
- await printData(await client.call("ai.security-clue.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, queryMode: options.queryMode, gtsCodeList: maybeArray(options.gtsCode), source: maybeArray(options.source) }), parseOutputFormat(options.format), options.output);
367
- });
368
- ai.command("one-pager").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
369
- const client = await createClient();
370
- await printData(await client.call("ai.one-pager", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
371
- });
372
- ai.command("investment-logic").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
373
- const client = await createClient();
374
- await printData(await client.call("ai.investment-logic", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
375
- });
376
- ai.command("peer-comparison").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
377
- const client = await createClient();
378
- await printData(await client.call("ai.peer-comparison", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
379
- });
380
- ai.command("earnings-review").requiredOption("--security-code <code>").requiredOption("--period <period>", "Report period (e.g. 2025q3, 2025interim, 2025annual)").option("--wait", "Wait for content generation (blocking, up to 3 min)").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
381
- const client = await createClient();
284
+ }));
285
+ ai.command("security-clue").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").requiredOption("--start-time <datetime>").requiredOption("--end-time <datetime>").addOption(new Option("--query-mode <mode>").choices(["bySecurity", "byIndustry"]).makeOptionMandatory()).option("--gts-code <code>", "GTS code", collectList, []).option("--source <name>", "Source", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.security-clue.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, queryMode: options.queryMode, gtsCodeList: maybeArray(options.gtsCode), source: maybeArray(options.source) })));
286
+ ai.command("one-pager").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.one-pager", { securityCode: options.securityCode })));
287
+ ai.command("investment-logic").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.investment-logic", { securityCode: options.securityCode })));
288
+ ai.command("peer-comparison").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.peer-comparison", { securityCode: options.securityCode })));
289
+ ai.command("earnings-review").requiredOption("--security-code <code>").requiredOption("--period <period>", "Report period (e.g. 2025q3, 2025interim, 2025annual)").option("--wait", "Wait for content generation (blocking, up to 3 min)").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => withClient(async (client) => {
382
290
  const idResult = await client.call("ai.earnings-review.get-id", { securityCode: options.securityCode, period: options.period });
383
291
  const dataId = idResult?.dataId;
384
292
  if (!dataId) {
@@ -396,24 +304,16 @@ ai.command("earnings-review").requiredOption("--security-code <code>").requiredO
396
304
  process.stderr.write(`Content not available after ${POLL_MAX_ATTEMPTS} attempts. Try again later with: gangtise ai earnings-review-check --data-id ${dataId}\n`);
397
305
  process.exitCode = 1;
398
306
  }
399
- });
400
- ai.command("earnings-review-check").requiredOption("--data-id <id>", "dataId from earnings-review").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
401
- const client = await createClient();
402
- await checkAsyncContent(client, "ai.earnings-review.get-content", options.dataId, parseOutputFormat(options.format), options.output);
403
- });
404
- ai.command("theme-tracking").requiredOption("--theme-id <id>", "Theme ID (use lookup theme-id list)").requiredOption("--date <date>", "Date (yyyy-MM-dd)").option("--type <name>", "Report type: morning/night", collectList, []).option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
405
- const client = await createClient();
307
+ }));
308
+ ai.command("earnings-review-check").requiredOption("--data-id <id>", "dataId from earnings-review").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => withClient((client) => checkAsyncContent(client, "ai.earnings-review.get-content", options.dataId, parseOutputFormat(options.format), options.output)));
309
+ ai.command("theme-tracking").requiredOption("--theme-id <id>", "Theme ID (use lookup theme-id list)").requiredOption("--date <date>", "Date (yyyy-MM-dd)").option("--type <name>", "Report type: morning/night", collectList, []).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => {
406
310
  const typeList = options.type.length ? options.type : undefined;
407
- await printData(await client.call("ai.theme-tracking", { themeId: options.themeId, date: options.date, type: typeList }), parseOutputFormat(options.format), options.output);
408
- });
409
- ai.command("research-outline").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
410
- const client = await createClient();
411
- await printData(await client.call("ai.research-outline", { securityCode: options.securityCode }), parseOutputFormat(options.format), options.output);
412
- });
413
- ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--category <name>", "Report type: morningBriefing/noonBriefing/afternoonFlash/eveningBriefing", collectList, []).option("--with-related-securities", "Include related securities info").option("--no-with-related-securities", "Exclude related securities info").option("--with-close-reading", "Include close reading content").option("--no-with-close-reading", "Exclude close reading content").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
414
- const client = await createClient();
311
+ return client.call("ai.theme-tracking", { themeId: options.themeId, date: options.date, type: typeList });
312
+ }));
313
+ ai.command("research-outline").requiredOption("--security-code <code>").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.research-outline", { securityCode: options.securityCode })));
314
+ ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--category <name>", "Report type: morningBriefing/noonBriefing/afternoonFlash/eveningBriefing", collectList, []).option("--with-related-securities", "Include related securities info").option("--no-with-related-securities", "Exclude related securities info").option("--with-close-reading", "Include close reading content").option("--no-with-close-reading", "Exclude close reading content").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => {
415
315
  const ALL_CATEGORIES = ["morningBriefing", "noonBriefing", "afternoonFlash", "eveningBriefing"];
416
- await printData(await client.call("ai.hot-topic", {
316
+ return client.call("ai.hot-topic", {
417
317
  from: parseFrom(options.from),
418
318
  size: parseSize(options.size),
419
319
  startDate: options.startDate,
@@ -421,26 +321,19 @@ ai.command("hot-topic").option("--from <number>", "Starting offset", "0").option
421
321
  categoryList: options.category.length > 0 ? options.category : ALL_CATEGORIES,
422
322
  withRelatedSecurities: options.withRelatedSecurities === false ? undefined : true,
423
323
  withCloseReading: options.withCloseReading === false ? undefined : true,
424
- }), parseOutputFormat(options.format), options.output);
425
- });
426
- ai.command("management-discuss-announcement").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension: businessOperation/financialPerformance/developmentAndRisk/all").choices(["businessOperation", "financialPerformance", "developmentAndRisk", "all"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
427
- const client = await createClient();
428
- await printData(await client.call("ai.management-discuss-announcement", {
429
- reportDate: options.reportDate,
430
- securityCode: options.securityCode,
431
- discussionDimension: options.dimension,
432
- }), parseOutputFormat(options.format), options.output);
433
- });
434
- ai.command("management-discuss-earnings-call").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension").choices(["businessOperation", "financialPerformance", "developmentAndRisk"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
435
- const client = await createClient();
436
- await printData(await client.call("ai.management-discuss-earnings-call", {
437
- reportDate: options.reportDate,
438
- securityCode: options.securityCode,
439
- discussionDimension: options.dimension,
440
- }), parseOutputFormat(options.format), options.output);
441
- });
442
- ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint text (max 1000 chars)").option("--wait", "Wait for content generation (blocking, up to 3 min)").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
443
- const client = await createClient();
324
+ });
325
+ }));
326
+ ai.command("management-discuss-announcement").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension: businessOperation/financialPerformance/developmentAndRisk/all").choices(["businessOperation", "financialPerformance", "developmentAndRisk", "all"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.management-discuss-announcement", {
327
+ reportDate: options.reportDate,
328
+ securityCode: options.securityCode,
329
+ discussionDimension: options.dimension,
330
+ })));
331
+ ai.command("management-discuss-earnings-call").requiredOption("--report-date <date>", "Report date (yyyy-MM-dd, e.g. 2025-06-30)").requiredOption("--security-code <code>", "Security code (e.g. 000001.SZ)").addOption(new Option("--dimension <name>", "Discussion dimension").choices(["businessOperation", "financialPerformance", "developmentAndRisk"]).makeOptionMandatory()).option("--format <format>", "Output format", "json").option("--output <path>").action((options) => emit(options, (client) => client.call("ai.management-discuss-earnings-call", {
332
+ reportDate: options.reportDate,
333
+ securityCode: options.securityCode,
334
+ discussionDimension: options.dimension,
335
+ })));
336
+ ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint text (max 1000 chars)").option("--wait", "Wait for content generation (blocking, up to 3 min)").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => withClient(async (client) => {
444
337
  const idResult = await client.call("ai.viewpoint-debate.get-id", { viewpoint: options.viewpoint });
445
338
  const dataId = idResult?.dataId;
446
339
  if (!dataId) {
@@ -458,86 +351,52 @@ ai.command("viewpoint-debate").requiredOption("--viewpoint <text>", "Viewpoint t
458
351
  process.stderr.write(`Content not available after ${POLL_MAX_ATTEMPTS} attempts. Try again later with: gangtise ai viewpoint-debate-check --data-id ${dataId}\n`);
459
352
  process.exitCode = 1;
460
353
  }
461
- });
462
- ai.command("viewpoint-debate-check").requiredOption("--data-id <id>", "dataId from viewpoint-debate").option("--format <format>", "Output format", "json").option("--output <path>").action(async (options) => {
463
- const client = await createClient();
464
- await checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseOutputFormat(options.format), options.output);
465
- });
354
+ }));
355
+ ai.command("viewpoint-debate-check").requiredOption("--data-id <id>", "dataId from viewpoint-debate").option("--format <format>", "Output format", "json").option("--output <path>").action((options) => withClient((client) => checkAsyncContent(client, "ai.viewpoint-debate.get-content", options.dataId, parseOutputFormat(options.format), options.output)));
466
356
  const reference = new Command("reference").description("Reference data APIs");
467
- reference.command("securities-search").requiredOption("--keyword <text>", "Search keyword (name/code/pinyin/English)").option("--category <type>", "Category: stock/dr/index/fund", collectList, []).option("--top <number>", "Max results (default: 10, max: 10)", "10").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
468
- const client = await createClient();
469
- await printData(await client.call("reference.securities-search", {
470
- keyword: options.keyword,
471
- category: options.category.length ? options.category : undefined,
472
- top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
473
- }), parseOutputFormat(options.format), options.output);
474
- });
357
+ reference.command("securities-search").requiredOption("--keyword <text>", "Search keyword (name/code/pinyin/English)").option("--category <type>", "Category: stock/dr/index/fund", collectList, []).option("--top <number>", "Max results (default: 10, max: 10)", "10").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("reference.securities-search", {
358
+ keyword: options.keyword,
359
+ category: options.category.length ? options.category : undefined,
360
+ top: parseNumberOption(options.top, "--top", { integer: true, min: 1 }),
361
+ })));
475
362
  program.addCommand(reference);
476
363
  const vault = new Command("vault").description("Vault APIs");
477
- vault.command("drive-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--file-type <number>", "File type", collectNumberList, []).option("--space-type <number>", "Space type", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
478
- const client = await createClient();
479
- await printData(await client.call("vault.drive.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, fileTypeList: options.fileType.length ? options.fileType : undefined, spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), parseOutputFormat(options.format), options.output, { endpointKey: "vault.drive.list", idField: "fileId" });
480
- });
481
- vault.command("drive-download").requiredOption("--file-id <id>").option("--output <path>").action(async (options) => {
482
- const client = await createClient();
364
+ vault.command("drive-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--file-type <number>", "File type", collectNumberList, []).option("--space-type <number>", "Space type", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.drive.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, fileTypeList: options.fileType.length ? options.fileType : undefined, spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.drive.list", idField: "fileId" }));
365
+ vault.command("drive-download").requiredOption("--file-id <id>").option("--output <path>").action((options) => withClient(async (client) => {
483
366
  await runDownload(client, "vault.drive.download", { fileId: options.fileId }, {
484
367
  output: options.output,
485
368
  fallbackName: `file-${options.fileId}`,
486
369
  resolveOutputPath: (result) => resolveTitle(client, result, "vault.drive.list", "fileId", options.fileId),
487
370
  });
488
- });
489
- vault.command("record-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--category <name>", "Recording type: upload/link/mobile/gtNote/pc/share", collectList, []).option("--space-type <number>", "Space type: 1=my records / 2=tenant records", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
490
- const client = await createClient();
491
- await printData(await client.call("vault.record.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, categoryList: maybeArray(options.category), spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), parseOutputFormat(options.format), options.output, { endpointKey: "vault.record.list", idField: "recordId" });
492
- });
493
- vault.command("record-download").requiredOption("--record-id <id>").requiredOption("--content-type <type>", "Content type: original/asr/summary").option("--output <path>").action(async (options) => {
494
- const client = await createClient();
371
+ }));
372
+ vault.command("record-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--category <name>", "Recording type: upload/link/mobile/gtNote/pc/share", collectList, []).option("--space-type <number>", "Space type: 1=my records / 2=tenant records", collectNumberList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.record.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, categoryList: maybeArray(options.category), spaceTypeList: options.spaceType.length ? options.spaceType : undefined }), { endpointKey: "vault.record.list", idField: "recordId" }));
373
+ vault.command("record-download").requiredOption("--record-id <id>").requiredOption("--content-type <type>", "Content type: original/asr/summary").option("--output <path>").action((options) => withClient(async (client) => {
495
374
  await runDownload(client, "vault.record.download", { recordId: options.recordId, contentType: options.contentType }, {
496
375
  output: options.output,
497
376
  fallbackName: `record-${options.recordId}`,
498
377
  resolveOutputPath: (result) => resolveTitle(client, result, "vault.record.list", "recordId", options.recordId),
499
378
  });
500
- });
501
- vault.command("my-conference-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--research-area <id>", "Research area ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
502
- const client = await createClient();
503
- await printData(await client.call("vault.my-conference.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution), categoryList: maybeArray(options.category) }), parseOutputFormat(options.format), options.output, { endpointKey: "vault.my-conference.list", idField: "conferenceId" });
504
- });
505
- vault.command("my-conference-download").requiredOption("--conference-id <id>").requiredOption("--content-type <type>", "Content type: asr/summary").option("--output <path>").action(async (options) => {
506
- const client = await createClient();
379
+ }));
380
+ vault.command("my-conference-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--research-area <id>", "Research area ID", collectList, []).option("--security <code>", "Security code", collectList, []).option("--institution <id>", "Institution ID", collectList, []).option("--category <name>", "Conference category: earningsCall/strategyMeeting/fundRoadshow/shareholdersMeeting/maMeeting/specialMeeting/companyAnalysis/industryAnalysis/other", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.my-conference.list", { from: parseFrom(options.from), size: parseSize(options.size), startTime: options.startTime, endTime: options.endTime, keyword: options.keyword, researchAreaList: maybeArray(options.researchArea), securityList: maybeArray(options.security), institutionList: maybeArray(options.institution), categoryList: maybeArray(options.category) }), { endpointKey: "vault.my-conference.list", idField: "conferenceId" }));
381
+ vault.command("my-conference-download").requiredOption("--conference-id <id>").requiredOption("--content-type <type>", "Content type: asr/summary").option("--output <path>").action((options) => withClient(async (client) => {
507
382
  await runDownload(client, "vault.my-conference.download", { conferenceId: options.conferenceId, contentType: options.contentType }, {
508
383
  output: options.output,
509
384
  fallbackName: `conference-${options.conferenceId}`,
510
385
  resolveOutputPath: (result) => resolveTitle(client, result, "vault.my-conference.list", "conferenceId", options.conferenceId),
511
386
  });
512
- });
513
- vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Message type: text/image/documents/url", collectList, []).option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
514
- const client = await createClient();
515
- await printData(await client.call("vault.wechat-message.list", buildWechatMessageListBody(options)), parseOutputFormat(options.format), options.output);
516
- });
517
- vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Rows to return", "20").option("--room-name <name>", "WeChat group name; repeat or comma-separate for multiple names", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
518
- const client = await createClient();
519
- await printData(await client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options)), parseOutputFormat(options.format), options.output);
520
- });
521
- vault.command("stock-pool-list").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
522
- const client = await createClient();
523
- await printData(await client.call("vault.stock-pool.list", {}), parseOutputFormat(options.format), options.output);
524
- });
525
- vault.command("stock-pool-stocks").option("--pool-id <id>", "Pool ID; repeat for multiple; use 'all' for all pools", collectList, ["all"]).option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
526
- const client = await createClient();
527
- await printData(await client.call("vault.stock-pool.stocks", { poolIdList: options.poolId }), parseOutputFormat(options.format), options.output);
528
- });
387
+ }));
388
+ vault.command("wechat-message-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Total rows to return; omit to fetch all").option("--start-time <datetime>").option("--end-time <datetime>").option("--keyword <text>").option("--security <code>", "Security code (e.g. 000001.SZ)", collectList, []).option("--wechat-group-id <id>", "WeChat group ID", collectList, []).option("--industry <id>", "Industry ID", collectList, []).option("--category <name>", "Message type: text/image/documents/url", collectList, []).option("--tag <name>", "Tag: roadShow/research/strategyMeeting/meetingSummary/industryComment/companyComment/earningsReview", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-message.list", buildWechatMessageListBody(options))));
389
+ vault.command("wechat-chatroom-list").option("--from <number>", "Starting offset", "0").option("--size <number>", "Rows to return", "20").option("--room-name <name>", "WeChat group name; repeat or comma-separate for multiple names", collectList, []).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.wechat-chatroom.list", buildWechatChatroomListBody(options))));
390
+ vault.command("stock-pool-list").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.list", {})));
391
+ vault.command("stock-pool-stocks").option("--pool-id <id>", "Pool ID; repeat for multiple; use 'all' for all pools", collectList, ["all"]).option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("vault.stock-pool.stocks", { poolIdList: options.poolId })));
529
392
  program.addCommand(vault);
530
393
  program.addCommand(ai);
531
394
  const alternative = new Command("alternative").description("Alternative data APIs");
532
- alternative.command("edb-search").requiredOption("--keyword <text>", "Search keyword (e.g. '空调')").option("--limit <number>", "Max results (default: 100, max: 200)", "100").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
533
- const client = await createClient();
534
- await printData(await client.call("alternative.edb-search", {
535
- keyword: options.keyword,
536
- limit: parseNumberOption(options.limit, "--limit", { integer: true, min: 1 }),
537
- }), parseOutputFormat(options.format), options.output);
538
- });
539
- alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (repeat, max 10)", collectList, []).requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)").requiredOption("--end-date <date>", "End date (yyyy-MM-dd)").option("--format <format>", "Output format", "table").option("--output <path>").action(async (options) => {
540
- const client = await createClient();
395
+ alternative.command("edb-search").requiredOption("--keyword <text>", "Search keyword (e.g. '空调')").option("--limit <number>", "Max results (default: 100, max: 200)", "100").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => emit(options, (client) => client.call("alternative.edb-search", {
396
+ keyword: options.keyword,
397
+ limit: parseNumberOption(options.limit, "--limit", { integer: true, min: 1 }),
398
+ })));
399
+ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (repeat, max 10)", collectList, []).requiredOption("--start-date <date>", "Start date (yyyy-MM-dd)").requiredOption("--end-date <date>", "End date (yyyy-MM-dd)").option("--format <format>", "Output format", "table").option("--output <path>").action((options) => withClient(async (client) => {
541
400
  const raw = await client.call("alternative.edb-data", {
542
401
  indicatorIdList: options.indicatorId,
543
402
  startDate: options.startDate,
@@ -552,7 +411,7 @@ alternative.command("edb-data").option("--indicator-id <id>", "Indicator ID (rep
552
411
  data = { list, total: list.length };
553
412
  }
554
413
  await printData(data, parseOutputFormat(options.format), options.output);
555
- });
414
+ }));
556
415
  program.addCommand(alternative);
557
416
  program.command("raw").description("Raw API calls").addCommand(new Command("call").argument("<endpointKey>").option("--body <json>").option("--query <key=value>", "Query string pair", collectKeyValue, {}).option("--format <format>", "Output format", "json").option("--output <path>").action(async (endpointKey, options) => {
558
417
  const endpoint = ENDPOINTS[endpointKey];
@@ -49,6 +49,27 @@ export class GangtiseClient {
49
49
  await writeTokenCache(this.config.tokenCachePath, cache);
50
50
  return accessToken;
51
51
  }
52
+ /**
53
+ * On a recoverable auth error (expired/invalid token codes), force a one-time
54
+ * token refresh and re-throw as retryable so withRetry replays the request.
55
+ * Otherwise — or once we've already retried this request — it's a no-op and
56
+ * the caller re-throws the original error. `authState` persists across the
57
+ * withRetry attempts so we only refresh once per logical request.
58
+ */
59
+ async refreshAuthIfRecoverable(error, useAuth, authState) {
60
+ if (useAuth
61
+ && !authState.retried
62
+ && error instanceof ApiError
63
+ && error.code
64
+ && AUTH_RETRY_CODES.has(error.code)
65
+ && this.config.accessKey
66
+ && this.config.secretKey) {
67
+ authState.retried = true;
68
+ this.memoCache = null;
69
+ await this.getAuthorizationHeader(true);
70
+ throw markRetryable(new ApiError(error.message, error.code, error.statusCode, error.details));
71
+ }
72
+ }
52
73
  isEnvelope(parsed) {
53
74
  if (!parsed || typeof parsed !== 'object')
54
75
  return false;
@@ -200,7 +221,7 @@ export class GangtiseClient {
200
221
  }
201
222
  const dispatcher = getDispatcher();
202
223
  const url = new URL(endpoint.path, this.config.baseUrl);
203
- let authRetried = false;
224
+ const authState = { retried: false };
204
225
  return withRetry(async () => {
205
226
  const headers = {
206
227
  'content-type': 'application/json',
@@ -236,18 +257,7 @@ export class GangtiseClient {
236
257
  return this.unwrapEnvelope(parsed, response.statusCode);
237
258
  }
238
259
  catch (error) {
239
- // Auto-recover from auth errors by forcing a token refresh once.
240
- if (useAuth
241
- && !authRetried
242
- && error instanceof ApiError
243
- && error.code
244
- && AUTH_RETRY_CODES.has(error.code)
245
- && (this.config.accessKey && this.config.secretKey)) {
246
- authRetried = true;
247
- this.memoCache = null;
248
- await this.getAuthorizationHeader(true);
249
- throw markRetryable(new ApiError(error.message, error.code, error.statusCode, error.details));
250
- }
260
+ await this.refreshAuthIfRecoverable(error, useAuth, authState);
251
261
  throw error;
252
262
  }
253
263
  }, {
@@ -265,6 +275,7 @@ export class GangtiseClient {
265
275
  Object.entries(query).forEach(([key, value]) => {
266
276
  url.searchParams.set(key, String(value));
267
277
  });
278
+ const authState = { retried: false };
268
279
  return withRetry(async () => {
269
280
  const authorization = await this.getAuthorizationHeader();
270
281
  const startedAt = Date.now();
@@ -292,7 +303,14 @@ export class GangtiseClient {
292
303
  if (response.statusCode >= 400) {
293
304
  this.throwHttpError(parsed, response.statusCode);
294
305
  }
295
- const data = this.unwrapEnvelope(parsed, response.statusCode);
306
+ let data;
307
+ try {
308
+ data = this.unwrapEnvelope(parsed, response.statusCode);
309
+ }
310
+ catch (error) {
311
+ await this.refreshAuthIfRecoverable(error, true, authState);
312
+ throw error;
313
+ }
296
314
  if (data && typeof data === 'object' && 'url' in data && typeof data.url === 'string') {
297
315
  return { url: String(data.url), contentType };
298
316
  }
@@ -67,21 +67,13 @@ function renderCsv(rows) {
67
67
  return "";
68
68
  }
69
69
  const columns = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
70
- const escape = (value) => {
71
- if (/^[=+\-@\t\r]/.test(value)) {
72
- value = "'" + value;
73
- }
74
- if (/[",\n]/.test(value)) {
75
- return `"${value.replaceAll("\"", "\"\"")}"`;
76
- }
77
- return value;
78
- };
79
70
  const header = columns.join(",");
80
- const body = rows.map((row) => columns.map((column) => escape(formatScalar(row[column]))).join(","));
71
+ const body = rows.map((row) => columns.map((column) => csvEscape(formatScalar(row[column]))).join(","));
81
72
  return [header, ...body].join("\n");
82
73
  }
83
74
  export function renderOutput(value, format) {
84
- const rows = toRows(value);
75
+ // toRows is computed lazily per branch: json never needs it, and jsonl only
76
+ // falls back to it when the value isn't already a {list}/array.
85
77
  switch (format) {
86
78
  case "json":
87
79
  return JSON.stringify(value, null, 2);
@@ -89,15 +81,15 @@ export function renderOutput(value, format) {
89
81
  const items = value && typeof value === "object" && !Array.isArray(value) && Array.isArray(value.list)
90
82
  ? value.list
91
83
  : null;
92
- return (items ?? rows).map((item) => JSON.stringify(item)).join("\n");
84
+ return (items ?? toRows(value)).map((item) => JSON.stringify(item)).join("\n");
93
85
  }
94
86
  case "csv":
95
- return renderCsv(rows);
87
+ return renderCsv(toRows(value));
96
88
  case "markdown":
97
- return renderMarkdown(rows);
89
+ return renderMarkdown(toRows(value));
98
90
  case "table":
99
91
  default:
100
- return renderTable(rows);
92
+ return renderTable(toRows(value));
101
93
  }
102
94
  }
103
95
  /** Stream large jsonl/csv output row-by-row to avoid building a full string in memory. */
@@ -4,6 +4,9 @@ const DAY_MS = 86_400_000;
4
4
  * `--security all` queries so a 2-day A-share shard (~11K rows) isn't
5
5
  * silently truncated. Single-security queries are untouched. */
6
6
  const ALL_MARKET_LIMIT = 10_000;
7
+ /** Shard fan-out concurrency. Shares the GANGTISE_PAGE_CONCURRENCY knob with
8
+ * pagination (see transport/client) so one env var tunes all request fan-out. */
9
+ const SHARD_CONCURRENCY = Number(process.env.GANGTISE_PAGE_CONCURRENCY ?? 5) || 5;
7
10
  function parseDate(value) {
8
11
  // Accept yyyy-MM-dd; reject anything else so we can fall back to a single request.
9
12
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
@@ -62,7 +65,7 @@ export async function callKlineWithSharding(client, endpointKey, body, config) {
62
65
  if (process.env.GANGTISE_VERBOSE === "1" || process.env.GANGTISE_VERBOSE === "true") {
63
66
  process.stderr.write(`[gangtise] sharding ${endpointKey} into ${shards.length} requests (${config.shardDays} day(s) each)\n`);
64
67
  }
65
- const results = await runWithConcurrency(shards, config.concurrency ?? 5, async (shard) => {
68
+ const results = await runWithConcurrency(shards, config.concurrency ?? SHARD_CONCURRENCY, async (shard) => {
66
69
  return client.call(endpointKey, { ...allMarketBody, startDate: shard.startDate, endDate: shard.endDate });
67
70
  });
68
71
  let fieldList;
@@ -4,6 +4,15 @@ import path from "node:path";
4
4
  export const DEFAULT_TITLE_CACHE_PATH = path.join(os.homedir(), ".config", "gangtise", "title-cache.json");
5
5
  export const TITLE_LOOKUP_SIZE = 200;
6
6
  const TITLE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7
+ /**
8
+ * Hard cap on titles kept per endpoint. The cache only needs the recent working
9
+ * set so downloads can resolve friendly filenames; without a cap, `writeTitleCache`
10
+ * merges forever and the endpoint's `ts` refreshes on every write, so the TTL
11
+ * never expires it — the file grew to tens of MB in practice, and `resolveTitle`
12
+ * re-parses the whole thing on every download. Bounds the file to roughly
13
+ * (#cached endpoints × this × avg entry size).
14
+ */
15
+ export const MAX_TITLES_PER_ENDPOINT = 5_000;
7
16
  // Per-process in-memory snapshot of the cache. We read the file at most once,
8
17
  // merge subsequent writes in memory, and flush atomically. This avoids the
9
18
  // "read whole file → modify → write whole file" pattern firing on every list
@@ -46,10 +55,65 @@ async function flush(filePath) {
46
55
  throw error;
47
56
  }
48
57
  }
58
+ /**
59
+ * Trim `merged` down to `cap` entries. The freshly-written ids (`freshKeys`) are
60
+ * kept first since those are what the user just listed and is most likely to
61
+ * download; remaining capacity is filled from the rest. Returns `merged` as-is
62
+ * when already within the cap.
63
+ */
64
+ function capTitles(merged, freshKeys, cap) {
65
+ if (Object.keys(merged).length <= cap)
66
+ return merged;
67
+ const out = {};
68
+ let n = 0;
69
+ for (const k of freshKeys) {
70
+ if (n >= cap)
71
+ break;
72
+ if (k in merged && !(k in out)) {
73
+ out[k] = merged[k];
74
+ n++;
75
+ }
76
+ }
77
+ if (n < cap) {
78
+ for (const k of Object.keys(merged)) {
79
+ if (n >= cap)
80
+ break;
81
+ if (k in out)
82
+ continue;
83
+ out[k] = merged[k];
84
+ n++;
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+ /**
90
+ * Keep the cache bounded on every write: drop endpoint entries past their TTL
91
+ * (lookups already ignore them) and cap each endpoint's titles. The just-written
92
+ * endpoint keeps its newest ids first; others over the cap are trimmed too, so a
93
+ * pre-existing oversized file shrinks on the next write.
94
+ */
95
+ function pruneCache(data, freshEndpoint, freshKeys) {
96
+ const now = Date.now();
97
+ for (const key of Object.keys(data)) {
98
+ const entry = data[key];
99
+ if (!entry || typeof entry.ts !== "number" || now - entry.ts > TITLE_CACHE_TTL_MS) {
100
+ delete data[key];
101
+ continue;
102
+ }
103
+ if (!entry.titles || typeof entry.titles !== "object") {
104
+ delete data[key];
105
+ continue;
106
+ }
107
+ if (Object.keys(entry.titles).length > MAX_TITLES_PER_ENDPOINT) {
108
+ entry.titles = capTitles(entry.titles, key === freshEndpoint ? freshKeys : [], MAX_TITLES_PER_ENDPOINT);
109
+ }
110
+ }
111
+ }
49
112
  export async function writeTitleCache(endpoint, titles, filePath = DEFAULT_TITLE_CACHE_PATH) {
50
113
  const data = await loadInto(filePath);
51
114
  const existing = data[endpoint]?.titles ?? {};
52
115
  data[endpoint] = { titles: { ...existing, ...titles }, ts: Date.now() };
116
+ pruneCache(data, endpoint, Object.keys(titles));
53
117
  dirty = true;
54
118
  // Coalesce concurrent writes: the in-flight flush picks up everything dirty.
55
119
  if (pendingWrite)
@@ -1,2 +1,2 @@
1
1
  // Auto-generated — DO NOT EDIT
2
- export const CLI_VERSION = "0.14.2";
2
+ export const CLI_VERSION = "0.14.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gangtise-openapi-cli",
3
- "version": "0.14.2",
3
+ "version": "0.14.3",
4
4
  "description": "CLI for Gangtise OpenAPI",
5
5
  "license": "MIT",
6
6
  "repository": {