gangtise-openapi-cli 0.34.1 → 0.36.0

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.
@@ -1,7 +1,5 @@
1
1
  import { ValidationError } from "./errors.js";
2
2
  export function splitCsv(value) {
3
- // Also split on full-width ",": voice-input IMEs produce it, and an unsplit
4
- // "600519,000858" goes to the API as one bogus code with no local hint.
5
3
  return value
6
4
  .split(/[,,]/)
7
5
  .map((item) => item.trim())
@@ -62,9 +60,6 @@ export function collectKeyValue(value, previous = {}) {
62
60
  export function maybeArray(value) {
63
61
  return value.length > 0 ? value : undefined;
64
62
  }
65
- /** True when `latest` is strictly newer than `current` (numeric per-segment
66
- * compare). Plain inequality would nag "update available" during the
67
- * just-published window while the registry still serves the previous version. */
68
63
  export function isVersionNewer(latest, current) {
69
64
  const parse = (v) => v.split(".").map(Number);
70
65
  const a = parse(latest);
@@ -79,9 +74,6 @@ export function isVersionNewer(latest, current) {
79
74
  }
80
75
  return false;
81
76
  }
82
- // Whitelist for enum-valued repeatable options. Only used where the server was
83
- // probed NOT to reject bad values (it silently ignores the filter or returns
84
- // empty instead) — endpoints that answer 100003 keep server-side validation.
85
77
  export function parseChoiceList(values, optionName, allowed) {
86
78
  for (const value of values) {
87
79
  if (!allowed.includes(value)) {
@@ -90,71 +82,39 @@ export function parseChoiceList(values, optionName, allowed) {
90
82
  }
91
83
  return maybeArray(values);
92
84
  }
93
- const YYYY_MM_DD = /^\d{4}-\d{2}-\d{2}$/;
94
- /**
95
- * Strict `YYYY-MM-DD` guard for Quote/Fundamental date options.
96
- *
97
- * Beyond the documented shape the server accepts two extra year-last formats
98
- * whose day/month order is *opposite* to each other (probed 2026-07-20 on
99
- * quote/kline/daily and fundamental/balance-sheet; other groups sharing these
100
- * flags were not probed, so the guard is applied uniformly as the safe default):
101
- *
102
- * "07/01/2026" -> 2026-01-07 slash reads DD/MM/YYYY
103
- * "07-01-2026" -> 2026-07-01 hyphen reads MM-DD-YYYY
104
- *
105
- * Same three digits, six months apart, both HTTP 200, and nothing in the
106
- * response echoes which date the server actually used. Confirmed by the
107
- * complement: "25/12/2026" parses while "12/25/2026" errors, and the hyphen
108
- * forms behave exactly the other way round. Since the CLI cannot know which
109
- * reading was meant, it forwards only the unambiguous form.
110
- *
111
- * Other unambiguous shapes (`20260701`, `2026/07/01`) are rejected too, even
112
- * though the server handles them — one accepted form beats a per-shape allowlist
113
- * that has to be re-probed per endpoint group. The message says which form is
114
- * wanted rather than claiming the input itself was ambiguous.
115
- *
116
- * Datetime options (`--start-time`) are guarded separately: pass-through ones by
117
- * `parseDatetimeOption` (a timezone-free field check that returns the string as-is),
118
- * and the two conversion endpoints (A-share announcement / knowledge-batch) by
119
- * `parseTimestamp13`.
120
- */
85
+ const YEAR_FIRST_DATE = /^(\d{4})([-/]?)(\d{2})\2(\d{2})$/;
86
+ function normalizeYearFirstDate(value) {
87
+ const parts = YEAR_FIRST_DATE.exec(value);
88
+ return parts ? `${parts[1]}-${parts[3]}-${parts[4]}` : undefined;
89
+ }
121
90
  export function parseDateOption(value, optionName) {
122
- if (!YYYY_MM_DD.test(value)) {
123
- throw new ValidationError(`Invalid ${optionName}: expected YYYY-MM-DD, got "${value}" — only that form is forwarded; some endpoints silently misread other layouts (e.g. "07/01/2026") as a different day`);
91
+ const normalized = normalizeYearFirstDate(value);
92
+ if (normalized === undefined) {
93
+ throw new ValidationError(`Invalid ${optionName}: expected YYYY-MM-DD (YYYY/MM/DD and YYYYMMDD also accepted), got "${value}" — year-last layouts are refused because the API reads them month-first (US convention): "01-07-2026" means 7 January, not 1 July`);
124
94
  }
125
- // Shape alone lets 2026-02-30 / 2026-13-01 through; round-trip to reject those.
126
- // Built from the ISO string, not Date.UTC(y,...), whose two-digit-year mapping
127
- // would turn a valid year 0050 into 1950 and report it as a non-existent date.
128
- const [year, month, day] = value.split("-").map(Number);
129
- const parsed = new Date(`${value}T00:00:00Z`);
95
+ const [year, month, day] = normalized.split("-").map(Number);
96
+ const parsed = new Date(`${normalized}T00:00:00Z`);
130
97
  if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) {
131
98
  throw new ValidationError(`Invalid ${optionName}: "${value}" is not a real calendar date`);
132
99
  }
133
- return value;
100
+ return normalized;
134
101
  }
135
- /** Commander argParser factory — `.option("--start-date <date>", desc, dateArg("--start-date"))`. */
136
102
  export function dateArg(optionName) {
137
103
  return (value) => parseDateOption(value, optionName);
138
104
  }
139
- /** `yyyy-MM-dd` with an optional ` HH:mm[:ss]` / `THH:mm[:ss]` tail. Anything else
140
- * is rejected rather than handed to `new Date()`: V8's fallback parser accepts the
141
- * same year-last shapes the server does but reads them the OTHER way round —
142
- * `07/01/2026` is July 1 to V8 and January 7 to the server, `25/12/2026` is invalid
143
- * to V8 and valid to the server (both probed 2026-07-20). Since `announcement list`
144
- * converts locally while its HK/US siblings pass the string through, an open
145
- * fallback made the same flag mean two dates six months apart across sibling
146
- * commands, silently and with exit 0. */
147
- const LOCAL_DATETIME = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
148
- /** Field-level datetime validation, timezone-free — a real calendar day plus a
149
- * valid clock time, judged by arithmetic alone (no Date construction, so no
150
- * dependence on the client's timezone or DST). This is what the pass-through guard
151
- * needs: a string the CLI forwards verbatim must be judged on its fields, not on
152
- * whether the local zone happens to contain that wall-clock instant. */
105
+ const LOCAL_DATETIME = /^(\d{4})([-/]?)(\d{2})\2(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/;
106
+ function normalizeDatetime(value) {
107
+ const parts = LOCAL_DATETIME.exec(value);
108
+ if (!parts)
109
+ return value;
110
+ const [, y, sep, mo, d] = parts;
111
+ return sep === "" ? `${y}-${mo}-${d}${value.slice(8)}` : `${y}-${mo}-${d}${value.slice(10)}`;
112
+ }
153
113
  function datetimeFieldsValid(value) {
154
114
  const parts = LOCAL_DATETIME.exec(value);
155
115
  if (!parts)
156
116
  return false;
157
- const [, y, mo, d, hh = "0", mi = "0", ss = "0"] = parts;
117
+ const [, y, , mo, d, hh = "0", mi = "0", ss = "0"] = parts;
158
118
  const year = Number(y), month = Number(mo), day = Number(d);
159
119
  if (month < 1 || month > 12)
160
120
  return false;
@@ -164,12 +124,6 @@ function datetimeFieldsValid(value) {
164
124
  return false;
165
125
  return Number(hh) <= 23 && Number(mi) <= 59 && Number(ss) <= 59;
166
126
  }
167
- /** A 10-digit seconds or 13-digit millis epoch, normalized to millis. Judged by
168
- * digit count, NOT magnitude: a `> 1e12` test sends a real 13-digit `1000000000000`
169
- * (which equals 1e12) down the seconds branch, and lets Number() coerce scientific
170
- * / hex / whitespace-padded / 11–12–14-digit inputs through as a "timestamp" — all
171
- * of which then convert wrong or get rejected by the server. Exactly 10 or 13
172
- * digits, nothing else. */
173
127
  function epochMillis(value) {
174
128
  if (/^\d{13}$/.test(value))
175
129
  return Number(value);
@@ -185,17 +139,10 @@ export function toTimestamp13(value) {
185
139
  return ts;
186
140
  if (!datetimeFieldsValid(value))
187
141
  return undefined;
188
- // `new Date("yyyy-MM-dd")` parses as UTC midnight while `new Date("yyyy-MM-dd
189
- // HH:mm:ss")` parses as local time — for CST users the two forms would differ by 8
190
- // hours. Build from local components so both mean the same wall-clock day.
191
142
  const parts = LOCAL_DATETIME.exec(value);
192
- const [, y, mo, d, hh = "0", mi = "0", ss = "0"] = parts;
143
+ const [, y, , mo, d, hh = "0", mi = "0", ss = "0"] = parts;
193
144
  const year = Number(y);
194
145
  const dt = new Date(year, Number(mo) - 1, Number(d), Number(hh), Number(mi), Number(ss));
195
- // Full round-trip on every field: rejects the two inputs that cannot convert
196
- // faithfully — the two-digit-year remap (new Date(50,…) → 1950) and a DST gap (a
197
- // wall-clock time the local zone skips: 02:30 on a US spring-forward morning, or
198
- // 02:15 in Lord Howe's 30-minute gap, which only a minute-level check catches).
199
146
  const faithful = dt.getFullYear() === year && dt.getMonth() === Number(mo) - 1
200
147
  && dt.getDate() === Number(d) && dt.getHours() === Number(hh)
201
148
  && dt.getMinutes() === Number(mi) && dt.getSeconds() === Number(ss);
@@ -206,53 +153,29 @@ export function parseTimestamp13(value, optionName) {
206
153
  return undefined;
207
154
  const parsed = toTimestamp13(value);
208
155
  if (parsed === undefined) {
209
- throw new ValidationError(`Invalid ${optionName}: expected a Unix timestamp or "YYYY-MM-DD" optionally with " HH:mm[:ss]" (space or T separator), got "${value}" — year-last forms are refused because Node and the API read their day/month order the opposite way round`);
156
+ throw new ValidationError(`Invalid ${optionName}: expected a Unix timestamp or "YYYY-MM-DD" optionally with " HH:mm[:ss]" (space or T separator; YYYY/MM/DD and YYYYMMDD also accepted), got "${value}" — year-last layouts are refused because the API reads them month-first (US convention): "01-07-2026" means 7 January, not 1 July`);
210
157
  }
211
158
  return parsed;
212
159
  }
213
- /**
214
- * Guard for datetime options forwarded to the server AS A STRING (never converted
215
- * to a timestamp): the pass-through Insight/Vault list endpoints echo the string
216
- * verbatim, and probing 2026-07-21 showed they misread year-last separators exactly
217
- * like the date endpoints — `insight research list` read `07/01/2026` as 2026-01-07
218
- * but `07-01-2026` as 2026-07-01, a half-year apart, both HTTP 200 with nothing in
219
- * the response flagging it. Accept a finite epoch or a well-formed `YYYY-MM-DD
220
- * [ HH:mm[:ss]]` and return the ORIGINAL string unchanged.
221
- *
222
- * Validated with `datetimeFieldsValid`, NOT `toTimestamp13`: the latter's local Date
223
- * round-trip would reject a DST-gap string (e.g. `2026-03-08 02:30:00` under
224
- * America/New_York) that the server accepts — the CLI forwards this string as-is and
225
- * the server resolves it in its own zone, so the client's timezone must not decide
226
- * validity. Distinct from `parseTimestamp13`, which DOES convert (A-share
227
- * announcement / knowledge-batch want epoch millis, where an unrepresentable local
228
- * instant genuinely cannot convert).
229
- */
230
160
  export function parseDatetimeOption(value, optionName) {
231
- if (epochMillis(value) === undefined && !datetimeFieldsValid(value)) {
232
- throw new ValidationError(`Invalid ${optionName}: expected a Unix timestamp or "YYYY-MM-DD" optionally with " HH:mm[:ss]" (space or T separator), got "${value}" — year-last forms are refused because the API reads their day/month order differently per separator`);
161
+ if (epochMillis(value) !== undefined)
162
+ return value;
163
+ if (!datetimeFieldsValid(value)) {
164
+ throw new ValidationError(`Invalid ${optionName}: expected a Unix timestamp or "YYYY-MM-DD" optionally with " HH:mm[:ss]" (space or T separator; YYYY/MM/DD and YYYYMMDD also accepted), got "${value}" — year-last layouts are refused because the API reads them month-first (US convention): "01-07-2026" means 7 January, not 1 July`);
233
165
  }
234
- return value;
166
+ return normalizeDatetime(value);
235
167
  }
236
- /** Commander argParser factory for pass-through datetime options — same role as
237
- * `dateArg`, but allows an optional time part and keeps the string as-is. */
238
168
  export function datetimeArg(optionName) {
239
169
  return (value) => parseDatetimeOption(value, optionName);
240
170
  }
241
- /** Machine-local calendar date as `yyyy-MM-dd`, for CLI "default: today" options.
242
- * `new Date().toISOString().slice(0,10)` renders the UTC day — for CST users a
243
- * pre-08:00 "today" resolves to yesterday. Anchoring to local components matches
244
- * toTimestamp13's local-midnight convention. */
245
171
  export function localDateString(d) {
246
172
  const month = String(d.getMonth() + 1).padStart(2, "0");
247
173
  const day = String(d.getDate()).padStart(2, "0");
248
174
  return `${d.getFullYear()}-${month}-${day}`;
249
175
  }
250
- // Parse repeatable `"<lhs>:key=value"` specs into per-lhs parameter lists.
251
- // The left-hand side is an indicator code for cross-section / time-series and a
252
- // screener variable (F1, F2…) for the screener, so it stays an opaque string
253
- // here. Multiple specs for the same lhs accumulate, first-seen order.
254
176
  function parseParamSpecs(specs, option, syntax) {
255
177
  const groups = new Map();
178
+ const noQueryDate = new Set();
256
179
  for (const spec of specs) {
257
180
  const colon = spec.indexOf(":");
258
181
  const rest = colon === -1 ? "" : spec.slice(colon + 1);
@@ -260,6 +183,14 @@ function parseParamSpecs(specs, option, syntax) {
260
183
  const lhs = colon === -1 ? "" : spec.slice(0, colon).trim();
261
184
  const paramKey = eq === -1 ? "" : rest.slice(0, eq).trim();
262
185
  const paramValue = eq === -1 ? "" : rest.slice(eq + 1).trim();
186
+ if (rest.trim() === "") {
187
+ if (!lhs)
188
+ throw new ValidationError(`Invalid ${option}: expected "${syntax}", got "${spec}"`);
189
+ noQueryDate.add(lhs);
190
+ if (!groups.has(lhs))
191
+ groups.set(lhs, []);
192
+ continue;
193
+ }
263
194
  if (!lhs || !paramKey) {
264
195
  throw new ValidationError(`Invalid ${option}: expected "${syntax}", got "${spec}"`);
265
196
  }
@@ -270,27 +201,19 @@ function parseParamSpecs(specs, option, syntax) {
270
201
  }
271
202
  params.push({ paramKey, paramValue });
272
203
  }
273
- return groups;
204
+ return { groups, noQueryDate };
274
205
  }
275
- // Parse repeatable `--indicator-param "code:key=value"` specs into the nested
276
- // indicatorParamList the EDE cross-section / time-series endpoints expect.
277
206
  export function parseIndicatorParams(specs) {
278
207
  if (specs.length === 0)
279
208
  return undefined;
280
- return [...parseParamSpecs(specs, "--indicator-param", "code:key=value")].map(([indicatorCode, parameters]) => ({ indicatorCode, parameters }));
209
+ const { groups, noQueryDate } = parseParamSpecs(specs, "--indicator-param", "code:key=value");
210
+ return [...groups].map(([indicatorCode, parameters]) => (noQueryDate.has(indicatorCode)
211
+ ? { indicatorCode, parameters, noQueryDate: true }
212
+ : { indicatorCode, parameters }));
281
213
  }
282
- /** Screener variables are `F` + a positive integer; the server rejects anything
283
- * else, and a typo'd variable would otherwise surface as an opaque expression
284
- * error rather than pointing at the binding that is wrong. */
285
214
  const SCREENER_FIELD = /^F[1-9][0-9]*$/;
286
- /** Variable references inside a screener expression, e.g. the F1/F2 in
287
- * `F1 >= 800 && F2 <= 30`. String literals are stripped first, so a
288
- * `contains 'F2 系列'` operand is not mistaken for a reference. */
289
215
  const SCREENER_FIELD_REF = /\bF[1-9][0-9]*\b/g;
290
216
  const SCREENER_STRING_LITERAL = /'[^']*'|"[^"]*"/g;
291
- /** Split an expression into `(`, `)`, `&&`, `||` and the atoms between them.
292
- * String literals are copied verbatim so a `||` or an `F2` inside one is never
293
- * mistaken for syntax. */
294
217
  function tokenizeScreenerExpression(src) {
295
218
  const tokens = [];
296
219
  let atom = "";
@@ -325,20 +248,6 @@ function tokenizeScreenerExpression(src) {
325
248
  flush();
326
249
  return tokens;
327
250
  }
328
- /** Whether the expression still has a path that could have been evaluated, given
329
- * the variables the response actually returned a column for.
330
- *
331
- * The boolean structure decides, not the mere presence of a `||`:
332
- * - a term is evaluable when every variable it names has a column;
333
- * - `A && B` needs BOTH sides — a missing mandatory conjunct is an unprovable
334
- * claim even if the other side is a disjunction;
335
- * - `A || B` needs only one — a row can legitimately match through one operand
336
- * while the other is not evaluable at all (probed 2026-08-03: `F1 > 0 || F2
337
- * > 0` over 09992.HK, where `finc_pe_ttm` has no HK coverage, matches on F2).
338
- *
339
- * So `F1 && (F2 || F3)` missing F1, and `F1 || F2` missing both, are both
340
- * unevaluable and must fail — checking only for a `||` anywhere would let them
341
- * through. */
342
251
  export function screenerExpressionIsEvaluable(expression, present) {
343
252
  const tokens = tokenizeScreenerExpression(expression ?? "");
344
253
  let pos = 0;
@@ -351,13 +260,10 @@ export function screenerExpressionIsEvaluable(expression, present) {
351
260
  return value;
352
261
  }
353
262
  const atom = tokens[pos++] ?? "";
354
- // A term naming no variable (a literal comparison) is always evaluable.
355
263
  return (atom.replace(SCREENER_STRING_LITERAL, "").match(SCREENER_FIELD_REF) ?? []).every((ref) => present.has(ref));
356
264
  };
357
265
  const and = () => {
358
266
  let value = unit();
359
- // Evaluate both sides before combining: short-circuiting would leave the
360
- // parser mid-expression.
361
267
  while (tokens[pos] === "&&") {
362
268
  pos += 1;
363
269
  const right = unit();
@@ -376,18 +282,11 @@ export function screenerExpressionIsEvaluable(expression, present) {
376
282
  };
377
283
  return or();
378
284
  }
379
- /** Variables the expression actually filters on, string literals stripped. These
380
- * are the bindings whose VALUES the result depends on — a column missing for one
381
- * of them means the filter cannot be shown to have been applied. */
382
285
  export function screenerExpressionFields(expression) {
383
286
  return (expression ?? "").replace(SCREENER_STRING_LITERAL, "").match(SCREENER_FIELD_REF) ?? [];
384
287
  }
385
- // Parse the screener's `--indicator "F1:code"` bindings and merge each one's
386
- // `--indicator-param "F1:key=value"` specs. Params key off the variable, not the
387
- // code, because the same indicator may legitimately appear under two variables
388
- // with different parameters (e.g. the same price on two dates).
389
288
  export function parseScreenerIndicators(bindings, paramSpecs, expression) {
390
- const params = parseParamSpecs(paramSpecs, "--indicator-param", "F1:key=value");
289
+ const { groups: params, noQueryDate } = parseParamSpecs(paramSpecs, "--indicator-param", "F1:key=value");
391
290
  const indicators = new Map();
392
291
  for (const spec of bindings) {
393
292
  const colon = spec.indexOf(":");
@@ -402,18 +301,15 @@ export function parseScreenerIndicators(bindings, paramSpecs, expression) {
402
301
  if (indicators.has(field)) {
403
302
  throw new ValidationError(`Duplicate --indicator variable "${field}": each variable must bind exactly one indicator`);
404
303
  }
405
- indicators.set(field, { field, indicatorCode, parameters: params.get(field) ?? [] });
304
+ indicators.set(field, noQueryDate.has(field)
305
+ ? { field, indicatorCode, parameters: params.get(field) ?? [], noQueryDate: true }
306
+ : { field, indicatorCode, parameters: params.get(field) ?? [] });
406
307
  }
407
- // A param for an unbound variable is silently dropped by the server, so the
408
- // query would run with a filter the caller believes is applied but is not.
409
308
  for (const field of params.keys()) {
410
309
  if (!indicators.has(field)) {
411
310
  throw new ValidationError(`--indicator-param references "${field}", which no --indicator binds`);
412
311
  }
413
312
  }
414
- // The server does reject this (100003), but only after a round trip — and the
415
- // symmetric mistake (binding a variable the expression never uses) it accepts
416
- // silently while still billing the extra column.
417
313
  for (const ref of screenerExpressionFields(expression)) {
418
314
  if (!indicators.has(ref)) {
419
315
  throw new ValidationError(`--expression references "${ref}", which no --indicator binds`);
@@ -1,44 +1,26 @@
1
1
  import { ApiError } from "./errors.js";
2
2
  import { printData } from "./printer.js";
3
3
  import { isTransientError } from "./transport.js";
4
- // 14 attempts with exponential backoff (5s→30s cap) ≈ 316s total wait budget.
5
4
  export const POLL_MAX_ATTEMPTS = 14;
6
5
  const POLL_INITIAL_DELAY_MS = 5_000;
7
6
  const POLL_MAX_DELAY_MS = 30_000;
8
7
  export function nextPollDelayMs(attempt) {
9
- // 5s, 8s, 13s, 20s, 30s, 30s, ...
10
8
  const grown = POLL_INITIAL_DELAY_MS * 1.6 ** (attempt - 1);
11
9
  return Math.min(POLL_MAX_DELAY_MS, Math.round(grown));
12
10
  }
13
- // Probed 2026-07-20 against a real viewpoint-debate job: the async endpoints are
14
- // still entirely on the legacy codes — 410110 "正在生成中" and 410111 "生成失败",
15
- // both string-typed, both HTTP 400, neither carrying the new `errorType` field.
16
- // The 2026-07-17 spec renumbers them to 140001 RESULT_GENERATING (409) and
17
- // 140002 PROCESSING_FAILED (500); those are listed ahead of the switchover
18
- // because the failure mode is expensive and silent — a `--wait` that does not
19
- // recognize the pending code aborts the poll on a job already billed 50 credits.
20
11
  const PENDING_CODES = new Set(["410110", "140001"]);
21
12
  const FAILED_CODES = new Set(["410111", "140002"]);
22
- /** Shared by the AI content endpoints and `tool file-parse` — both report
23
- * "still generating" with the same codes. */
24
13
  export function isAsyncPending(error) {
25
14
  return error instanceof ApiError && error.code !== undefined && PENDING_CODES.has(error.code);
26
15
  }
27
16
  function isAsyncFailed(error) {
28
17
  return error instanceof ApiError && error.code !== undefined && FAILED_CODES.has(error.code);
29
18
  }
30
- /** Terminal failures are swallowed here rather than rethrown, so this line is the
31
- * user's only record of them — it has to carry what the global handler in cli.ts
32
- * would have printed (code, msg, traceId). Without the traceId the failure is
33
- * unreportable to Gangtise support, and README promises every error line has one. */
34
19
  function terminalFailureLine(error) {
35
20
  const api = error instanceof ApiError ? error : undefined;
36
21
  const code = api?.code ? ` ${api.code}` : "";
37
22
  const trace = api?.traceId ? ` [trace ${api.traceId}]` : "";
38
23
  const detail = api?.message ? `: ${api.message}` : "";
39
- // Name the *submit* endpoint explicitly: re-running a `*-check` is a free
40
- // lookup, it is resubmitting the generation job that re-bills 50 credits for a
41
- // verdict that will not change (probed 2026-07-20 on sensitive content).
42
24
  return `Content generation failed (terminal${code})${trace}${detail}\n`
43
25
  + "This dataId is final — re-checking it will not change. Resubmitting the generation task bills again for the same result; change the parameters first.\n";
44
26
  }
@@ -57,10 +39,6 @@ export async function pollAsyncContent(client, getContentEndpoint, dataId, forma
57
39
  return "failed";
58
40
  }
59
41
  if (!isAsyncPending(error)) {
60
- // AI generation windows are exactly when the server is busiest: one 5xx
61
- // (after the client's own retries) must not void minutes of waiting —
62
- // the dataId is still valid. Transient errors consume this attempt and
63
- // polling continues; anything else (no credits, bad params) aborts.
64
42
  if (!isTransientError(error))
65
43
  throw error;
66
44
  const msg = error instanceof Error ? error.message : String(error);
@@ -15,13 +15,6 @@ export async function readTokenCache(filePath) {
15
15
  return null;
16
16
  }
17
17
  }
18
- /**
19
- * Return a display-safe copy of a token cache for `auth status`: any field whose
20
- * name matches a credential pattern (token / key / secret / password / credential)
21
- * is replaced with "<redacted>" so the raw bearer token — or any unknown credential
22
- * field the cache file might carry (apiKey, privateKey, …) — is never printed; all
23
- * other metadata (expiresAt, userName, productCode, …) is preserved.
24
- */
25
18
  export function redactTokenCache(cache) {
26
19
  if (!cache)
27
20
  return null;
@@ -34,11 +27,6 @@ export function redactTokenCache(cache) {
34
27
  }
35
28
  export async function writeTokenCache(filePath, cache) {
36
29
  await fs.mkdir(path.dirname(filePath), { recursive: true });
37
- // Write to a fresh 0600 temp file then rename over the target. Writing the bearer
38
- // token straight to token.json would (a) keep an existing file's lax perms — the
39
- // `mode` option only applies on creation, so a follow-up chmod still leaves a brief
40
- // world-readable window — and (b) risk a truncated file on crash. A temp file is
41
- // 0600 from the first byte and rename is atomic, carrying the 0600 perms over.
42
30
  const tmp = `${filePath}.tmp-${randomUUID()}`;
43
31
  await fs.writeFile(tmp, JSON.stringify(cache, null, 2), { encoding: "utf8", mode: 0o600 });
44
32
  try {
@@ -57,8 +45,6 @@ export function isTokenCacheValid(cache, bufferSeconds = 300) {
57
45
  return cache.expiresAt - bufferSeconds > now;
58
46
  }
59
47
  export function normalizeToken(token) {
60
- // Case-insensitive prefix check: GANGTISE_TOKEN="bearer xxx" must become
61
- // "Bearer xxx", not the silently-invalid "Bearer bearer xxx".
62
48
  const prefix = /^bearer\s+/i.exec(token);
63
49
  return `Bearer ${prefix ? token.slice(prefix[0].length) : token}`;
64
50
  }