@docmana/sdk 0.3.4 → 0.4.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +123 -211
  3. package/dist/api/execution-result.d.ts +3 -0
  4. package/dist/api/execution-status.d.ts +6 -0
  5. package/dist/api/run-flow.d.ts +6 -0
  6. package/dist/cli.mjs +225 -250
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/client.d.ts +21 -0
  9. package/dist/config.d.ts +21 -0
  10. package/dist/errors.d.ts +55 -0
  11. package/dist/files/resolve-input.d.ts +11 -0
  12. package/dist/http/http-client.d.ts +26 -0
  13. package/dist/index.d.ts +4 -335
  14. package/dist/index.js +167 -291
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +167 -291
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/models/docmana-api-document-execution-result.model.d.ts +9 -0
  19. package/dist/models/docmana-api-document-request.model.d.ts +15 -0
  20. package/dist/models/docmana-api-execution-result.model.d.ts +24 -0
  21. package/dist/models/docmana-api-node-result.model.d.ts +28 -0
  22. package/dist/models/docmana-classification-result.model.d.ts +3 -0
  23. package/dist/models/docmana-conclusion-result.model.d.ts +3 -0
  24. package/dist/models/docmana-cross-validation-result.model.d.ts +3 -0
  25. package/dist/models/docmana-document-mapping.model.d.ts +4 -0
  26. package/dist/models/docmana-document-result.model.d.ts +11 -0
  27. package/dist/models/docmana-execution-result.model.d.ts +15 -0
  28. package/dist/models/docmana-extraction-result.model.d.ts +3 -0
  29. package/dist/models/docmana-metadata-extraction-result.model.d.ts +3 -0
  30. package/dist/models/docmana-node-result.model.d.ts +7 -0
  31. package/dist/models/docmana-score-node-result.model.d.ts +5 -0
  32. package/dist/models/docmana-validation-result.model.d.ts +3 -0
  33. package/dist/models/document-content.model.d.ts +4 -0
  34. package/dist/models/execution-progress.model.d.ts +4 -0
  35. package/dist/models/execution-status.enum.d.ts +1 -0
  36. package/dist/models/flow-configs.model.d.ts +16 -0
  37. package/dist/models/flow-edge.model.d.ts +4 -0
  38. package/dist/models/flow-node-type.enum.d.ts +1 -0
  39. package/dist/models/flow-node.model.d.ts +33 -0
  40. package/dist/models/flow.model.d.ts +11 -0
  41. package/dist/models/index.d.ts +26 -0
  42. package/dist/models/node-translation-result.model.d.ts +7 -0
  43. package/dist/models/physical-document.model.d.ts +9 -0
  44. package/dist/models/virtual-document.model.d.ts +10 -0
  45. package/dist/polling/poll.d.ts +15 -0
  46. package/dist/types.d.ts +76 -0
  47. package/package.json +17 -14
  48. package/dist/index.d.cts +0 -335
package/dist/cli.mjs CHANGED
@@ -61,7 +61,14 @@ var DocmanaAbortError = class extends DocmanaError {
61
61
  super(message, { code: "aborted" });
62
62
  }
63
63
  };
64
- function errorFromResponse(status, message, requestId) {
64
+ var DocmanaRateLimitError = class extends DocmanaError {
65
+ retryAfterMs;
66
+ constructor(message, opts = {}) {
67
+ super(message, { ...opts, code: "rate_limited" });
68
+ this.retryAfterMs = opts.retryAfterMs;
69
+ }
70
+ };
71
+ function errorFromResponse(status, message, requestId, retryAfterMs) {
65
72
  const opts = { status, requestId };
66
73
  switch (status) {
67
74
  case 400:
@@ -72,6 +79,8 @@ function errorFromResponse(status, message, requestId) {
72
79
  return new DocmanaPermissionError(message, opts);
73
80
  case 404:
74
81
  return new DocmanaNotFoundError(message, opts);
82
+ case 429:
83
+ return new DocmanaRateLimitError(message, { ...opts, retryAfterMs });
75
84
  default:
76
85
  return new DocmanaError(message, opts);
77
86
  }
@@ -159,29 +168,33 @@ var TokenManager = class {
159
168
 
160
169
  // src/config.ts
161
170
  var DEFAULTS = {
162
- apiBaseUrl: "https://api.docmana.ai",
163
171
  tokenEndpoint: "https://4fe70f5b-e013-4f65-9fa7-3109a33beba5.ciamlogin.com/4fe70f5b-e013-4f65-9fa7-3109a33beba5/oauth2/v2.0/token",
164
172
  scope: "api://d781e6ba-cc08-4618-8099-ad968abd2b9e/.default",
165
173
  timeoutMs: 3e5,
166
- pollIntervalMs: 2e3
174
+ pollIntervalMs: 2e3,
175
+ pollMaxIntervalMs: 1e4
167
176
  };
168
177
  function resolveConfig(config) {
169
- if (!config.clientId) throw new Error("DocmanaConfig.clientId is required");
170
- if (!config.clientSecret) throw new Error("DocmanaConfig.clientSecret is required");
171
- const apiBaseUrl = (config.apiBaseUrl ?? DEFAULTS.apiBaseUrl).replace(/\/+$/, "");
178
+ if (!config.apiBaseUrl) throw new Error("DocmanaConfig.apiBaseUrl is required");
179
+ if (!config.accessToken && !config.getAccessToken) {
180
+ throw new Error("DocmanaConfig.accessToken or DocmanaConfig.getAccessToken is required");
181
+ }
182
+ const apiBaseUrl = normalizeApiBaseUrl(config.apiBaseUrl);
172
183
  return {
173
- clientId: config.clientId,
174
- clientSecret: config.clientSecret,
175
184
  apiBaseUrl,
176
- tokenEndpoint: config.tokenEndpoint ?? DEFAULTS.tokenEndpoint,
177
- scope: config.scope ?? DEFAULTS.scope,
185
+ accessToken: config.accessToken,
186
+ getAccessToken: config.getAccessToken,
178
187
  timeoutMs: config.timeoutMs ?? DEFAULTS.timeoutMs,
179
188
  pollIntervalMs: config.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
189
+ pollMaxIntervalMs: config.pollMaxIntervalMs ?? DEFAULTS.pollMaxIntervalMs,
180
190
  fetchImpl: config.fetch ?? fetch,
181
- headers: config.headers ?? {},
182
- tokenCache: config.tokenCache
191
+ headers: config.headers ?? {}
183
192
  };
184
193
  }
194
+ function normalizeApiBaseUrl(value) {
195
+ const trimmed = value.replace(/\/+$/, "");
196
+ return /\/v\d+$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
197
+ }
185
198
 
186
199
  // src/http/http-client.ts
187
200
  var HttpClient = class {
@@ -201,19 +214,23 @@ var HttpClient = class {
201
214
  }
202
215
  async requestRaw(method, path, options = {}) {
203
216
  let res = await this.send(method, path, options);
204
- if (res.status === 401) {
205
- this.opts.tokenManager.invalidate();
206
- res = await this.send(method, path, options);
217
+ if (res.status === 401 && this.opts.getAccessToken) {
218
+ res = await this.send(method, path, options, true);
207
219
  }
208
220
  if (!res.ok) {
209
221
  const requestId = res.headers.get("x-request-id") ?? void 0;
210
222
  const message = await this.extractMessage(res);
211
- throw errorFromResponse(res.status, message, requestId);
223
+ throw errorFromResponse(
224
+ res.status,
225
+ message,
226
+ requestId,
227
+ parseRetryAfterMs(res.headers.get("retry-after"))
228
+ );
212
229
  }
213
230
  return res;
214
231
  }
215
- async send(method, path, options) {
216
- const token = await this.opts.tokenManager.getToken();
232
+ async send(method, path, options, forceRefresh = false) {
233
+ const token = await this.resolveToken(forceRefresh);
217
234
  const url = new URL(this.opts.apiBaseUrl + path);
218
235
  for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
219
236
  const headers = {
@@ -235,6 +252,13 @@ var HttpClient = class {
235
252
  throw err;
236
253
  }
237
254
  }
255
+ async resolveToken(forceRefresh) {
256
+ const token = this.opts.getAccessToken ? await this.opts.getAccessToken(forceRefresh ? { forceRefresh: true } : void 0) : this.opts.accessToken;
257
+ if (!token) {
258
+ throw new DocmanaError("Missing Docmana access token", { code: "auth_missing" });
259
+ }
260
+ return token;
261
+ }
238
262
  async extractMessage(res) {
239
263
  try {
240
264
  const data = await res.clone().json();
@@ -244,6 +268,18 @@ var HttpClient = class {
244
268
  }
245
269
  }
246
270
  };
271
+ function parseRetryAfterMs(value) {
272
+ if (!value) return void 0;
273
+ const seconds = Number(value);
274
+ if (Number.isFinite(seconds) && seconds >= 0) {
275
+ return seconds * 1e3;
276
+ }
277
+ const retryAt = Date.parse(value);
278
+ if (!Number.isNaN(retryAt)) {
279
+ return Math.max(0, retryAt - Date.now());
280
+ }
281
+ return void 0;
282
+ }
247
283
 
248
284
  // src/files/resolve-input.ts
249
285
  import { readFile } from "fs/promises";
@@ -275,16 +311,19 @@ async function streamToBuffer(stream) {
275
311
  for await (const chunk of stream) chunks.push(Buffer.from(chunk));
276
312
  return Buffer.concat(chunks);
277
313
  }
314
+ function toBlobBytes(bytes) {
315
+ return new Uint8Array(bytes);
316
+ }
278
317
  async function resolveOne(input, fallbackName) {
279
318
  if (typeof input === "string" || isPathInput(input)) {
280
319
  const path = typeof input === "string" ? input : input.path;
281
320
  const data = await readFile(path);
282
321
  const filename = basename(path);
283
- return { data: new Blob([data]), filename, contentType: contentTypeFor(filename) };
322
+ return { data: new Blob([toBlobBytes(data)]), filename, contentType: contentTypeFor(filename) };
284
323
  }
285
324
  if (input instanceof Uint8Array) {
286
325
  return {
287
- data: new Blob([input]),
326
+ data: new Blob([toBlobBytes(input)]),
288
327
  filename: fallbackName,
289
328
  contentType: contentTypeFor(fallbackName)
290
329
  };
@@ -292,7 +331,7 @@ async function resolveOne(input, fallbackName) {
292
331
  if (isReadable(input)) {
293
332
  const buf = await streamToBuffer(input);
294
333
  return {
295
- data: new Blob([buf]),
334
+ data: new Blob([toBlobBytes(buf)]),
296
335
  filename: fallbackName,
297
336
  contentType: contentTypeFor(fallbackName)
298
337
  };
@@ -306,38 +345,43 @@ async function resolveInputs(input) {
306
345
  return Promise.all(list.map((item) => resolveOne(item, fallback)));
307
346
  }
308
347
 
309
- // src/api/upload.ts
310
- async function uploadFiles(http, parts, signal) {
348
+ // src/api/run-flow.ts
349
+ async function runFlow(http, flowId, parts, signal, useDraftVersion = false, context, callbackUrl, language) {
311
350
  const form = new FormData();
312
351
  for (const part of parts) {
313
352
  form.append("files", new File([part.data], part.filename, { type: part.contentType }));
314
353
  }
315
- const res = await http.requestJson("POST", "/upload", { body: form, signal });
316
- return [res.id];
317
- }
318
-
319
- // src/api/run-flow.ts
320
- async function runFlow(http, flowId, uuidFiles, signal, once = false, context) {
321
- const path = once ? `/flows/run_once_flow/${flowId}` : `/flows/run_flow/${flowId}`;
322
- const body = { uuid_files: uuidFiles };
323
- if (context !== void 0) body.json_context = context;
324
- const res = await http.requestJson("POST", path, {
325
- query: { async_mode: "true" },
326
- headers: { "content-type": "application/json" },
327
- body: JSON.stringify(body),
328
- signal
329
- });
330
- return res.execution_result_id;
354
+ if (context !== void 0) form.append("json_context", JSON.stringify(context));
355
+ if (callbackUrl !== void 0) form.append("callback_url", callbackUrl);
356
+ if (language !== void 0) form.append("language", language);
357
+ if (useDraftVersion) form.append("useDraftVersion", "true");
358
+ return http.requestJson(
359
+ "POST",
360
+ `/flows/${flowId}/execute-async`,
361
+ { body: form, signal }
362
+ );
331
363
  }
332
364
 
333
365
  // src/api/execution-status.ts
334
366
  async function getStatus(http, executionResultId, signal) {
335
- return http.requestJson("GET", `/flows/execution-status/${executionResultId}`, { signal });
367
+ const res = await http.requestRaw("GET", `/executions/${executionResultId}/status`, { signal });
368
+ const text = await res.text();
369
+ const body = text ? JSON.parse(text) : { status: "" };
370
+ const pollAfterMs = parsePositiveInt(res.headers.get("x-poll-after"));
371
+ return pollAfterMs === void 0 ? body : { ...body, pollAfterMs };
372
+ }
373
+ function parsePositiveInt(value) {
374
+ if (!value) return void 0;
375
+ const parsed = Number(value);
376
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
336
377
  }
337
378
 
338
379
  // src/api/execution-result.ts
339
- async function getResult(http, executionResultId, signal) {
340
- return http.requestJson("GET", `/flows/execution-result/${executionResultId}`, { signal });
380
+ async function getResult(http, executionResultId, signal, language) {
381
+ return http.requestJson("GET", `/executions/${executionResultId}/result`, {
382
+ query: language ? { language } : void 0,
383
+ signal
384
+ });
341
385
  }
342
386
 
343
387
  // src/polling/poll.ts
@@ -347,166 +391,42 @@ async function pollUntilTerminal(opts) {
347
391
  const sleep = opts.sleep ?? defaultSleep;
348
392
  const start = Date.now();
349
393
  const elapsed = opts.elapsed ?? (() => Date.now() - start);
394
+ const random = opts.random ?? Math.random;
395
+ let fallbackAttempt = 0;
350
396
  for (; ; ) {
351
397
  if (opts.signal?.aborted) throw new DocmanaAbortError("Polling aborted");
352
- const status = await opts.check();
398
+ let result;
399
+ try {
400
+ result = normalizePollResult(await opts.check());
401
+ } catch (err) {
402
+ if (!(err instanceof DocmanaRateLimitError)) throw err;
403
+ if (elapsed() >= opts.timeoutMs) {
404
+ throw new DocmanaTimeoutError(`Execution did not finish within ${opts.timeoutMs}ms`);
405
+ }
406
+ await sleep(err.retryAfterMs ?? opts.intervalMs);
407
+ continue;
408
+ }
409
+ const { status } = result;
353
410
  if (TERMINAL.has(status)) return status;
354
411
  if (elapsed() >= opts.timeoutMs) {
355
412
  throw new DocmanaTimeoutError(`Execution did not finish within ${opts.timeoutMs}ms`);
356
413
  }
357
- await sleep(opts.intervalMs);
414
+ const fallbackDelay = fullJitterDelay(
415
+ opts.intervalMs,
416
+ opts.maxIntervalMs,
417
+ fallbackAttempt,
418
+ random
419
+ );
420
+ fallbackAttempt += 1;
421
+ await sleep(result.nextDelayMs ?? fallbackDelay);
358
422
  }
359
423
  }
360
-
361
- // src/mappers/mapper.ts
362
- function extractNodeFields(rawNode, targetLanguage) {
363
- const metadataFields = /* @__PURE__ */ new Set([
364
- "node_id",
365
- "node_name",
366
- "node_type",
367
- "feature_name",
368
- "running_time",
369
- "status",
370
- "score",
371
- "feedback",
372
- "translations",
373
- "tokens",
374
- "input_tokens",
375
- "output_tokens",
376
- "errors",
377
- "mana",
378
- "model",
379
- "tool_calls",
380
- "prompt_id",
381
- "prompt",
382
- "prompt_name",
383
- "label",
384
- "scoreDescription",
385
- "score_description",
386
- "result",
387
- "documentType"
388
- ]);
389
- let sourceObj = rawNode;
390
- if (targetLanguage) {
391
- const translations = rawNode.translations;
392
- if (!translations || !translations[targetLanguage]) {
393
- throw new DocmanaError(
394
- `Translation for language '${targetLanguage}' is missing on node '${rawNode.node_name || rawNode.node_id}'`,
395
- { code: "translation_missing", status: 400 }
396
- );
397
- }
398
- sourceObj = translations[targetLanguage];
399
- }
400
- const dynamicFields = {};
401
- for (const [key, value] of Object.entries(sourceObj)) {
402
- if (!metadataFields.has(key)) {
403
- dynamicFields[key] = value;
404
- }
405
- }
406
- const sdkNode = {
407
- status: rawNode.status || "Unknown"
408
- };
409
- if (rawNode.errors && rawNode.errors.length > 0) {
410
- sdkNode.errors = rawNode.errors;
411
- }
412
- const nodeType = rawNode.node_type || "";
413
- const isScoreNode = /^(Validation|Classification|CrossValidation|Conclusion|Finish)$/i.test(
414
- nodeType
415
- );
416
- if (isScoreNode && rawNode.score !== void 0 && rawNode.score !== null) {
417
- sdkNode.score = rawNode.score;
418
- }
419
- const feedback = sourceObj.feedback ?? rawNode.feedback;
420
- if (feedback !== void 0 && feedback !== null) {
421
- sdkNode.feedback = feedback;
422
- }
423
- if (Object.keys(dynamicFields).length > 0) {
424
- sdkNode.data = dynamicFields;
425
- }
426
- return sdkNode;
424
+ function normalizePollResult(result) {
425
+ return typeof result === "string" ? { status: result } : result;
427
426
  }
428
- function mapExecutionResult(raw, targetLanguage) {
429
- const sdkResult = {
430
- status: raw.status || "Unknown",
431
- executionId: String(raw.execution_id || raw.executionResultId || raw.execution_result_id || ""),
432
- documents: {
433
- mappings: []
434
- }
435
- };
436
- if (raw.errors && raw.errors.length > 0) {
437
- sdkResult.errors = raw.errors;
438
- }
439
- const results = raw.results || [];
440
- for (const item of results) {
441
- const isDocSpecific = !!item.document;
442
- if (isDocSpecific && item.document) {
443
- const docReq = item.document;
444
- const docKey = docReq.doc_key || "unknown_doc";
445
- const docName = docReq.name || "";
446
- sdkResult.documents.mappings.push({
447
- name: docName,
448
- reference: docKey
449
- });
450
- const docResult = {
451
- status: item.status || docReq.status || "Unknown",
452
- extractions: {},
453
- validations: {}
454
- };
455
- const nodeResults = item.node_results || [];
456
- for (const node of nodeResults) {
457
- const nodeType = node.node_type || "";
458
- const nodeName = node.node_name || node.node_id || "unknown_node";
459
- if (/^Classification$/i.test(nodeType)) {
460
- const mappedNode = extractNodeFields(node, targetLanguage);
461
- if (!docResult.classifications) {
462
- docResult.classifications = {};
463
- }
464
- docResult.classifications[nodeName] = mappedNode;
465
- } else if (/^MetadataExtraction$/i.test(nodeType)) {
466
- docResult.metadataExtraction = extractNodeFields(node, targetLanguage);
467
- } else if (/^Extraction$/i.test(nodeType)) {
468
- if (docResult.extractions) {
469
- docResult.extractions[nodeName] = extractNodeFields(node, targetLanguage);
470
- }
471
- } else if (/^Validation$/i.test(nodeType)) {
472
- if (docResult.validations) {
473
- docResult.validations[nodeName] = extractNodeFields(node, targetLanguage);
474
- }
475
- }
476
- }
477
- if (docResult.extractions && Object.keys(docResult.extractions).length === 0) {
478
- delete docResult.extractions;
479
- }
480
- if (docResult.validations && Object.keys(docResult.validations).length === 0) {
481
- delete docResult.validations;
482
- }
483
- sdkResult.documents[docKey] = docResult;
484
- } else {
485
- const nodeResults = item.node_results || [];
486
- for (const node of nodeResults) {
487
- const nodeType = node.node_type || "";
488
- const nodeName = node.node_name || node.node_id || "unknown_node";
489
- if (/^CrossValidation$/i.test(nodeType)) {
490
- if (!sdkResult.crossValidations) {
491
- sdkResult.crossValidations = {};
492
- }
493
- sdkResult.crossValidations[nodeName] = extractNodeFields(
494
- node,
495
- targetLanguage
496
- );
497
- } else if (/^(Conclusion|Finish)$/i.test(nodeType)) {
498
- if (!sdkResult.conclusions) {
499
- sdkResult.conclusions = {};
500
- }
501
- sdkResult.conclusions[nodeName] = extractNodeFields(
502
- node,
503
- targetLanguage
504
- );
505
- }
506
- }
507
- }
508
- }
509
- return sdkResult;
427
+ function fullJitterDelay(intervalMs, maxIntervalMs, attempt, random) {
428
+ const capDelay = Math.min(maxIntervalMs, intervalMs * 2 ** attempt);
429
+ return Math.round(intervalMs + random() * (capDelay - intervalMs));
510
430
  }
511
431
 
512
432
  // src/client.ts
@@ -515,46 +435,79 @@ var Docmana = class {
515
435
  http;
516
436
  constructor(config) {
517
437
  this.config = resolveConfig(config);
518
- const tokenManager = new TokenManager({
519
- clientId: this.config.clientId,
520
- clientSecret: this.config.clientSecret,
521
- tokenEndpoint: this.config.tokenEndpoint,
522
- scope: this.config.scope,
523
- fetchImpl: this.config.fetchImpl,
524
- tokenCache: this.config.tokenCache
525
- });
526
438
  this.http = new HttpClient({
527
439
  apiBaseUrl: this.config.apiBaseUrl,
528
440
  fetchImpl: this.config.fetchImpl,
529
- tokenManager,
441
+ accessToken: this.config.accessToken,
442
+ getAccessToken: this.config.getAccessToken,
530
443
  headers: this.config.headers
531
444
  });
532
445
  }
533
446
  async runFlowAsync(flowId, input) {
534
447
  const parts = await resolveInputs(input);
535
- const fileIds = await uploadFiles(this.http, parts, input.signal);
536
- const executionResultId = await runFlow(
448
+ return runFlow(
537
449
  this.http,
538
450
  flowId,
539
- fileIds,
451
+ parts,
540
452
  input.signal,
541
- input.once ?? false,
542
- input.context
453
+ input.useDraftVersion ?? false,
454
+ input.context,
455
+ void 0,
456
+ input.language
457
+ );
458
+ }
459
+ async runFlowWithCallback(flowId, input) {
460
+ const parts = await resolveInputs(input);
461
+ return runFlow(
462
+ this.http,
463
+ flowId,
464
+ parts,
465
+ input.signal,
466
+ input.useDraftVersion ?? false,
467
+ input.context,
468
+ input.callbackUrl,
469
+ input.language
543
470
  );
544
- return { executionResultId, fileIds };
545
471
  }
546
472
  async getStatus(executionResultId, signal) {
547
473
  return getStatus(this.http, executionResultId, signal);
548
474
  }
549
475
  async getResult(executionResultId, signal, language) {
550
- const rawResult = await getResult(this.http, executionResultId, signal);
551
- return mapExecutionResult(rawResult, language);
476
+ return getResult(this.http, executionResultId, signal, language);
552
477
  }
553
478
  async runFlow(flowId, input) {
554
479
  const { executionResultId } = await this.runFlowAsync(flowId, input);
480
+ let attempt = 0;
481
+ const startedAt = Date.now();
555
482
  await pollUntilTerminal({
556
- check: async () => (await this.getStatus(executionResultId, input.signal)).status,
483
+ check: async () => {
484
+ attempt += 1;
485
+ try {
486
+ const statusResult = await this.getStatus(executionResultId, input.signal);
487
+ const status = statusResult.status;
488
+ const nextPollAfterMs = this.isTerminalStatus(status) ? void 0 : statusResult.pollAfterMs;
489
+ input.onPoll?.({
490
+ executionResultId,
491
+ status,
492
+ attempt,
493
+ elapsedMs: Date.now() - startedAt,
494
+ nextPollAfterMs
495
+ });
496
+ return { status, nextDelayMs: nextPollAfterMs };
497
+ } catch (err) {
498
+ if (err instanceof DocmanaRateLimitError) {
499
+ input.onRateLimit?.({
500
+ executionResultId,
501
+ attempt,
502
+ elapsedMs: Date.now() - startedAt,
503
+ retryAfterMs: err.retryAfterMs ?? input.pollIntervalMs ?? this.config.pollIntervalMs
504
+ });
505
+ }
506
+ throw err;
507
+ }
508
+ },
557
509
  intervalMs: input.pollIntervalMs ?? this.config.pollIntervalMs,
510
+ maxIntervalMs: input.pollMaxIntervalMs ?? this.config.pollMaxIntervalMs,
558
511
  timeoutMs: input.timeoutMs ?? this.config.timeoutMs,
559
512
  signal: input.signal
560
513
  });
@@ -568,6 +521,9 @@ var Docmana = class {
568
521
  }
569
522
  return result;
570
523
  }
524
+ isTerminalStatus(status) {
525
+ return status === "Completed" || status === "Failed";
526
+ }
571
527
  };
572
528
 
573
529
  // src/cli.ts
@@ -638,7 +594,7 @@ function buildProgram(env, io, clientFactory, deps) {
638
594
  const flow = program.command("flow").description(HELP.flow).action(() => {
639
595
  io.stdout(flow.helpInformation());
640
596
  });
641
- flow.command("run").description(HELP.run).argument("<flow-id>", "Docmana flow id").requiredOption("--files <csv>", "Comma-separated file paths to upload").option("--client-id <id>", "OAuth2 client id; defaults to DOCMANA_CLIENT_ID").option("--client-secret <secret>", "OAuth2 client secret; defaults to DOCMANA_CLIENT_SECRET").option("--api-url <url>", "Docmana API base URL; defaults to DOCMANA_API_URL").option("--token-url <url>", "OAuth2 token endpoint; defaults to DOCMANA_TOKEN_URL").option("--scope <scope>", "OAuth2 scope; defaults to DOCMANA_SCOPE").option("--organisation <id>", "Docmana organisation id; defaults to DOCMANA_ORGANISATION").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).option("--json", "Print the raw JSON result").option("--language <lang>", "Translate output to specified language").action(async (flowId, options) => {
597
+ flow.command("run").description(HELP.run).argument("<flow-id>", "Docmana flow id").requiredOption("--files <csv>", "Comma-separated file paths to upload").option("--api-url <url>", "Consumer API base URL; defaults to DOCMANA_API_URL").option("--access-token <token>", "Bearer token; defaults to DOCMANA_ACCESS_TOKEN").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).option("--json", "Print the raw JSON result").option("--language <lang>", "Translate output to specified language").option("--draft", "Run the current draft flow version").action(async (flowId, options) => {
642
598
  await runFlowCommand(flowId, options, env, io, clientFactory, deps);
643
599
  });
644
600
  const config = program.command("config").description(HELP.config).action(() => {
@@ -653,46 +609,35 @@ async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
653
609
  try {
654
610
  const files = parseFiles(options.files);
655
611
  const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
656
- const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
657
- const clientSecret = firstNonEmpty(
658
- options.clientSecret,
659
- env.DOCMANA_CLIENT_SECRET,
660
- cliConfig.clientSecret
661
- );
662
- const organisation = firstNonEmpty(
663
- options.organisation,
664
- env.DOCMANA_ORGANISATION,
665
- cliConfig.organisation
666
- );
667
612
  const apiBaseUrl = firstNonEmpty(options.apiUrl, env.DOCMANA_API_URL, cliConfig.apiBaseUrl);
668
- const tokenEndpoint = firstNonEmpty(
669
- options.tokenUrl,
670
- env.DOCMANA_TOKEN_URL,
671
- cliConfig.tokenEndpoint
672
- );
673
- const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope);
674
- if (!clientId)
675
- throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
676
- if (!clientSecret) {
677
- throw new CliUsageError(
678
- "Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET."
679
- );
680
- }
681
- if (!organisation) {
682
- throw new CliUsageError("Missing organisation. Use --organisation or DOCMANA_ORGANISATION.");
613
+ if (!apiBaseUrl) {
614
+ throw new CliUsageError("Missing API base URL. Use --api-url or DOCMANA_API_URL.");
683
615
  }
616
+ const tokenCache = createFileTokenCache(
617
+ resolveTokenCachePath(options.tokenCache, getCwd(deps))
618
+ );
619
+ const accessToken = firstNonEmpty(options.accessToken, env.DOCMANA_ACCESS_TOKEN) ?? await resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io);
684
620
  const client = clientFactory({
685
- clientId,
686
- clientSecret,
687
621
  apiBaseUrl,
688
- tokenEndpoint,
689
- scope,
690
- headers: { "X-Selected-Organization": organisation },
691
- tokenCache: createFileTokenCache(resolveTokenCachePath(options.tokenCache, getCwd(deps)))
622
+ accessToken
692
623
  });
693
624
  const result = await client.runFlow(flowId, {
694
625
  files: files.map((path) => ({ path })),
695
- language: options.language
626
+ language: options.language,
627
+ useDraftVersion: options.draft === true,
628
+ onPoll: (event) => {
629
+ const nextPoll = event.nextPollAfterMs === void 0 ? "" : `, next poll in ${formatElapsedSeconds(event.nextPollAfterMs)}`;
630
+ io.stderr(
631
+ `Polling execution ${event.executionResultId}: ${event.status} (attempt ${event.attempt}, elapsed ${formatElapsedSeconds(event.elapsedMs)}${nextPoll})
632
+ `
633
+ );
634
+ },
635
+ onRateLimit: (event) => {
636
+ io.stderr(
637
+ `Polling execution ${event.executionResultId}: rate limited (attempt ${event.attempt}, retrying in ${formatElapsedSeconds(event.retryAfterMs)})
638
+ `
639
+ );
640
+ }
696
641
  });
697
642
  if (options.json) {
698
643
  io.stdout(`${JSON.stringify(result, null, 2)}
@@ -722,10 +667,38 @@ async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
722
667
  throw err;
723
668
  }
724
669
  }
670
+ async function resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io) {
671
+ const cached = await readFreshCachedToken(tokenCache);
672
+ if (cached) return cached;
673
+ const clientId = firstNonEmpty(env.DOCMANA_CLIENT_ID, cliConfig.clientId);
674
+ const clientSecret = firstNonEmpty(env.DOCMANA_CLIENT_SECRET, cliConfig.clientSecret);
675
+ if (!clientId || !clientSecret) {
676
+ throw new CliUsageError(
677
+ "Missing access token. Use --access-token, DOCMANA_ACCESS_TOKEN, docmana login, or configure OAuth credentials."
678
+ );
679
+ }
680
+ const tokenManager = new TokenManager({
681
+ clientId,
682
+ clientSecret,
683
+ tokenEndpoint: firstNonEmpty(env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint,
684
+ scope: firstNonEmpty(env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope,
685
+ fetchImpl: deps.fetch ?? fetch,
686
+ tokenCache
687
+ });
688
+ const token = await tokenManager.getToken();
689
+ io.stderr("Generated new access token and updated the token cache.\n");
690
+ return token;
691
+ }
692
+ async function readFreshCachedToken(tokenCache) {
693
+ const cached = await tokenCache.read().catch(() => null);
694
+ if (!cached || Date.now() >= cached.expiresAt) return void 0;
695
+ return cached.accessToken;
696
+ }
725
697
  async function loginCommand(options, env, io, deps) {
726
698
  const auth = await resolveAuthConfig(options, env, deps);
727
699
  const tokenCachePath = resolveTokenCachePath(options.tokenCache, getCwd(deps));
728
700
  const tokenCache = createFileTokenCache(tokenCachePath);
701
+ const hadFreshCachedToken = Boolean(await readFreshCachedToken(tokenCache));
729
702
  const tokenManager = new TokenManager({
730
703
  clientId: auth.clientId,
731
704
  clientSecret: auth.clientSecret,
@@ -736,6 +709,9 @@ async function loginCommand(options, env, io, deps) {
736
709
  });
737
710
  await tokenManager.getToken();
738
711
  const cached = await tokenCache.read();
712
+ if (!hadFreshCachedToken) {
713
+ io.stdout("Generated new access token and updated the token cache.\n");
714
+ }
739
715
  io.stdout(`Logged in. Token cached at ${tokenCachePath}
740
716
  `);
741
717
  if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
@@ -774,7 +750,7 @@ async function configInitCommand(options, io, deps) {
774
750
  try {
775
751
  const config = {
776
752
  apiBaseUrl: await prompt("API base URL", {
777
- defaultValue: existing.apiBaseUrl ?? DEFAULTS.apiBaseUrl
753
+ defaultValue: existing.apiBaseUrl
778
754
  }),
779
755
  tokenEndpoint: await prompt("OAuth2 token endpoint", {
780
756
  defaultValue: existing.tokenEndpoint ?? DEFAULTS.tokenEndpoint
@@ -782,9 +758,6 @@ async function configInitCommand(options, io, deps) {
782
758
  scope: await prompt("OAuth2 scope", {
783
759
  defaultValue: existing.scope ?? DEFAULTS.scope
784
760
  }),
785
- organisation: await prompt("Organisation", {
786
- defaultValue: existing.organisation
787
- }),
788
761
  clientId: await prompt("Client id", {
789
762
  defaultValue: existing.clientId
790
763
  }),
@@ -833,7 +806,6 @@ function normalizeCliConfig(config) {
833
806
  apiBaseUrl: stringValue(config.apiBaseUrl),
834
807
  tokenEndpoint: stringValue(config.tokenEndpoint),
835
808
  scope: stringValue(config.scope),
836
- organisation: stringValue(config.organisation),
837
809
  clientId: stringValue(config.clientId),
838
810
  clientSecret: stringValue(config.clientSecret)
839
811
  };
@@ -909,6 +881,9 @@ function formatPrompt(label, options) {
909
881
  const defaultHint = options.defaultValue && !options.secret ? ` [${options.defaultValue}]` : options.defaultValue && options.secret ? " [current value hidden]" : "";
910
882
  return `${label}${defaultHint}: `;
911
883
  }
884
+ function formatElapsedSeconds(elapsedMs) {
885
+ return `${Math.floor(elapsedMs / 1e3)}s`;
886
+ }
912
887
  async function readStdinLines() {
913
888
  let input = "";
914
889
  for await (const chunk of process.stdin) input += String(chunk);