@docmana/sdk 0.3.5 → 0.8.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 (53) hide show
  1. package/CHANGELOG.md +33 -4
  2. package/README.md +175 -232
  3. package/dist/api/execution-result.d.ts +3 -0
  4. package/dist/api/execution-status.d.ts +6 -0
  5. package/dist/api/flow-definition.d.ts +3 -0
  6. package/dist/api/flow-metadata.d.ts +3 -0
  7. package/dist/api/list-flows.d.ts +3 -0
  8. package/dist/api/list-organizations.d.ts +3 -0
  9. package/dist/api/organization-documents.d.ts +7 -0
  10. package/dist/api/run-flow.d.ts +6 -0
  11. package/dist/cli.mjs +1022 -695
  12. package/dist/cli.mjs.map +1 -1
  13. package/dist/client.d.ts +26 -0
  14. package/dist/config.d.ts +21 -0
  15. package/dist/errors.d.ts +55 -0
  16. package/dist/files/resolve-input.d.ts +9 -0
  17. package/dist/http/http-client.d.ts +31 -0
  18. package/dist/index.d.ts +4 -349
  19. package/dist/index.js +323 -339
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +319 -335
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/models/docmana-api-document-execution-result.model.d.ts +9 -0
  24. package/dist/models/docmana-api-document-request.model.d.ts +15 -0
  25. package/dist/models/docmana-api-execution-result.model.d.ts +24 -0
  26. package/dist/models/docmana-api-node-result.model.d.ts +28 -0
  27. package/dist/models/docmana-classification-result.model.d.ts +3 -0
  28. package/dist/models/docmana-conclusion-result.model.d.ts +3 -0
  29. package/dist/models/docmana-cross-validation-result.model.d.ts +3 -0
  30. package/dist/models/docmana-document-mapping.model.d.ts +4 -0
  31. package/dist/models/docmana-document-result.model.d.ts +11 -0
  32. package/dist/models/docmana-execution-result.model.d.ts +15 -0
  33. package/dist/models/docmana-extraction-result.model.d.ts +3 -0
  34. package/dist/models/docmana-metadata-extraction-result.model.d.ts +3 -0
  35. package/dist/models/docmana-node-result.model.d.ts +7 -0
  36. package/dist/models/docmana-score-node-result.model.d.ts +5 -0
  37. package/dist/models/docmana-validation-result.model.d.ts +3 -0
  38. package/dist/models/document-content.model.d.ts +4 -0
  39. package/dist/models/execution-progress.model.d.ts +4 -0
  40. package/dist/models/execution-status.enum.d.ts +1 -0
  41. package/dist/models/flow-configs.model.d.ts +16 -0
  42. package/dist/models/flow-edge.model.d.ts +4 -0
  43. package/dist/models/flow-node-type.enum.d.ts +1 -0
  44. package/dist/models/flow-node.model.d.ts +33 -0
  45. package/dist/models/flow.model.d.ts +11 -0
  46. package/dist/models/index.d.ts +26 -0
  47. package/dist/models/node-translation-result.model.d.ts +7 -0
  48. package/dist/models/physical-document.model.d.ts +9 -0
  49. package/dist/models/virtual-document.model.d.ts +10 -0
  50. package/dist/polling/poll.d.ts +15 -0
  51. package/dist/types.d.ts +145 -0
  52. package/package.json +17 -14
  53. package/dist/index.d.cts +0 -349
package/dist/cli.mjs CHANGED
@@ -1,14 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/cli.ts
4
- import { existsSync, realpathSync } from "fs";
5
- import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
6
- import { dirname, resolve } from "path";
7
- import { createInterface } from "readline/promises";
3
+ // src/cli/cli.ts
4
+ import { realpathSync } from "fs";
5
+ import { resolve as resolve2 } from "path";
8
6
  import { fileURLToPath } from "url";
9
7
  import { Command, CommanderError } from "commander";
10
8
 
11
- // src/errors.ts
9
+ // src/sdk/config.ts
10
+ var DEFAULTS = {
11
+ tokenEndpoint: "https://4fe70f5b-e013-4f65-9fa7-3109a33beba5.ciamlogin.com/4fe70f5b-e013-4f65-9fa7-3109a33beba5/oauth2/v2.0/token",
12
+ scope: "api://d781e6ba-cc08-4618-8099-ad968abd2b9e/.default",
13
+ timeoutMs: 3e5,
14
+ pollIntervalMs: 2e3,
15
+ pollMaxIntervalMs: 1e4
16
+ };
17
+ function resolveConfig(config) {
18
+ if (!config.apiBaseUrl) throw new Error("DocmanaConfig.apiBaseUrl is required");
19
+ if (!config.accessToken && !config.getAccessToken) {
20
+ throw new Error("DocmanaConfig.accessToken or DocmanaConfig.getAccessToken is required");
21
+ }
22
+ const apiBaseUrl = normalizeApiBaseUrl(config.apiBaseUrl);
23
+ return {
24
+ apiBaseUrl,
25
+ accessToken: config.accessToken,
26
+ getAccessToken: config.getAccessToken,
27
+ timeoutMs: config.timeoutMs ?? DEFAULTS.timeoutMs,
28
+ pollIntervalMs: config.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
29
+ pollMaxIntervalMs: config.pollMaxIntervalMs ?? DEFAULTS.pollMaxIntervalMs,
30
+ fetchImpl: config.fetch ?? fetch,
31
+ headers: config.headers ?? {}
32
+ };
33
+ }
34
+ function normalizeApiBaseUrl(value) {
35
+ const trimmed = value.replace(/\/+$/, "");
36
+ return /\/v\d+$/i.test(trimmed) ? trimmed : `${trimmed}/v1`;
37
+ }
38
+
39
+ // src/sdk/errors.ts
12
40
  var DocmanaError = class extends Error {
13
41
  status;
14
42
  requestId;
@@ -61,7 +89,14 @@ var DocmanaAbortError = class extends DocmanaError {
61
89
  super(message, { code: "aborted" });
62
90
  }
63
91
  };
64
- function errorFromResponse(status, message, requestId) {
92
+ var DocmanaRateLimitError = class extends DocmanaError {
93
+ retryAfterMs;
94
+ constructor(message, opts = {}) {
95
+ super(message, { ...opts, code: "rate_limited" });
96
+ this.retryAfterMs = opts.retryAfterMs;
97
+ }
98
+ };
99
+ function errorFromResponse(status, message, requestId, retryAfterMs) {
65
100
  const opts = { status, requestId };
66
101
  switch (status) {
67
102
  case 400:
@@ -72,118 +107,14 @@ function errorFromResponse(status, message, requestId) {
72
107
  return new DocmanaPermissionError(message, opts);
73
108
  case 404:
74
109
  return new DocmanaNotFoundError(message, opts);
110
+ case 429:
111
+ return new DocmanaRateLimitError(message, { ...opts, retryAfterMs });
75
112
  default:
76
113
  return new DocmanaError(message, opts);
77
114
  }
78
115
  }
79
116
 
80
- // src/auth/token-manager.ts
81
- var SAFETY_MARGIN_MS = 6e4;
82
- var TokenManager = class {
83
- constructor(opts) {
84
- this.opts = opts;
85
- }
86
- opts;
87
- token = null;
88
- expiresAt = 0;
89
- inflight = null;
90
- invalidate() {
91
- this.token = null;
92
- this.expiresAt = 0;
93
- void this.opts.tokenCache?.clear().catch(() => void 0);
94
- }
95
- async getToken() {
96
- if (this.token && Date.now() < this.expiresAt) return this.token;
97
- const cached = await this.readCachedToken();
98
- if (cached) return cached;
99
- if (this.inflight) return this.inflight;
100
- this.inflight = this.acquire().finally(() => {
101
- this.inflight = null;
102
- });
103
- return this.inflight;
104
- }
105
- async acquire() {
106
- const body = new URLSearchParams({
107
- grant_type: "client_credentials",
108
- client_id: this.opts.clientId,
109
- client_secret: this.opts.clientSecret,
110
- scope: this.opts.scope
111
- });
112
- const res = await this.opts.fetchImpl(this.opts.tokenEndpoint, {
113
- method: "POST",
114
- headers: { "content-type": "application/x-www-form-urlencoded" },
115
- body: body.toString()
116
- });
117
- if (!res.ok) {
118
- throw new DocmanaAuthError("Failed to acquire Docmana access token", { status: res.status });
119
- }
120
- const json = await res.json();
121
- if (typeof json.access_token !== "string" || json.access_token.length === 0 || typeof json.expires_in !== "number" || !Number.isFinite(json.expires_in)) {
122
- throw new DocmanaAuthError("Malformed token response from Docmana CIAM", {
123
- status: res.status
124
- });
125
- }
126
- this.token = json.access_token;
127
- this.expiresAt = Date.now() + json.expires_in * 1e3 - SAFETY_MARGIN_MS;
128
- await this.writeCachedToken().catch(() => void 0);
129
- return this.token;
130
- }
131
- async readCachedToken() {
132
- if (!this.opts.tokenCache) return null;
133
- let entry;
134
- try {
135
- entry = await this.opts.tokenCache.read();
136
- } catch {
137
- return null;
138
- }
139
- if (!entry) return null;
140
- if (entry.clientId !== this.opts.clientId || entry.tokenEndpoint !== this.opts.tokenEndpoint || entry.scope !== this.opts.scope || Date.now() >= entry.expiresAt) {
141
- await this.opts.tokenCache.clear().catch(() => void 0);
142
- return null;
143
- }
144
- this.token = entry.accessToken;
145
- this.expiresAt = entry.expiresAt;
146
- return this.token;
147
- }
148
- async writeCachedToken() {
149
- if (!this.opts.tokenCache || !this.token) return;
150
- await this.opts.tokenCache.write({
151
- accessToken: this.token,
152
- expiresAt: this.expiresAt,
153
- clientId: this.opts.clientId,
154
- tokenEndpoint: this.opts.tokenEndpoint,
155
- scope: this.opts.scope
156
- });
157
- }
158
- };
159
-
160
- // src/config.ts
161
- var DEFAULTS = {
162
- apiBaseUrl: "https://api.docmana.ai",
163
- tokenEndpoint: "https://4fe70f5b-e013-4f65-9fa7-3109a33beba5.ciamlogin.com/4fe70f5b-e013-4f65-9fa7-3109a33beba5/oauth2/v2.0/token",
164
- scope: "api://d781e6ba-cc08-4618-8099-ad968abd2b9e/.default",
165
- timeoutMs: 3e5,
166
- pollIntervalMs: 2e3
167
- };
168
- 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(/\/+$/, "");
172
- return {
173
- clientId: config.clientId,
174
- clientSecret: config.clientSecret,
175
- apiBaseUrl,
176
- tokenEndpoint: config.tokenEndpoint ?? DEFAULTS.tokenEndpoint,
177
- scope: config.scope ?? DEFAULTS.scope,
178
- timeoutMs: config.timeoutMs ?? DEFAULTS.timeoutMs,
179
- pollIntervalMs: config.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
180
- fetchImpl: config.fetch ?? fetch,
181
- headers: config.headers ?? {},
182
- tokenCache: config.tokenCache
183
- };
184
- }
185
-
186
- // src/http/http-client.ts
117
+ // src/sdk/http/http-client.ts
187
118
  var HttpClient = class {
188
119
  constructor(opts) {
189
120
  this.opts = opts;
@@ -199,21 +130,29 @@ var HttpClient = class {
199
130
  throw new DocmanaError("Invalid JSON response from Docmana", { status: res.status });
200
131
  }
201
132
  }
133
+ async requestBytes(method, path, options = {}) {
134
+ const res = await this.requestRaw(method, path, options);
135
+ return { bytes: new Uint8Array(await res.arrayBuffer()), headers: res.headers };
136
+ }
202
137
  async requestRaw(method, path, options = {}) {
203
138
  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);
139
+ if (res.status === 401 && this.opts.getAccessToken) {
140
+ res = await this.send(method, path, options, true);
207
141
  }
208
142
  if (!res.ok) {
209
143
  const requestId = res.headers.get("x-request-id") ?? void 0;
210
144
  const message = await this.extractMessage(res);
211
- throw errorFromResponse(res.status, message, requestId);
145
+ throw errorFromResponse(
146
+ res.status,
147
+ message,
148
+ requestId,
149
+ parseRetryAfterMs(res.headers.get("retry-after"))
150
+ );
212
151
  }
213
152
  return res;
214
153
  }
215
- async send(method, path, options) {
216
- const token = await this.opts.tokenManager.getToken();
154
+ async send(method, path, options, forceRefresh = false) {
155
+ const token = await this.resolveToken(forceRefresh);
217
156
  const url = new URL(this.opts.apiBaseUrl + path);
218
157
  for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
219
158
  const headers = {
@@ -235,6 +174,13 @@ var HttpClient = class {
235
174
  throw err;
236
175
  }
237
176
  }
177
+ async resolveToken(forceRefresh) {
178
+ const token = this.opts.getAccessToken ? await this.opts.getAccessToken(forceRefresh ? { forceRefresh: true } : void 0) : this.opts.accessToken;
179
+ if (!token) {
180
+ throw new DocmanaError("Missing Docmana access token", { code: "auth_missing" });
181
+ }
182
+ return token;
183
+ }
238
184
  async extractMessage(res) {
239
185
  try {
240
186
  const data = await res.clone().json();
@@ -244,8 +190,20 @@ var HttpClient = class {
244
190
  }
245
191
  }
246
192
  };
193
+ function parseRetryAfterMs(value) {
194
+ if (!value) return void 0;
195
+ const seconds = Number(value);
196
+ if (Number.isFinite(seconds) && seconds >= 0) {
197
+ return seconds * 1e3;
198
+ }
199
+ const retryAt = Date.parse(value);
200
+ if (!Number.isNaN(retryAt)) {
201
+ return Math.max(0, retryAt - Date.now());
202
+ }
203
+ return void 0;
204
+ }
247
205
 
248
- // src/files/resolve-input.ts
206
+ // src/sdk/files/resolve-input.ts
249
207
  import { readFile } from "fs/promises";
250
208
  import { basename } from "path";
251
209
  var CONTENT_TYPES = {
@@ -270,310 +228,310 @@ function isReadable(x) {
270
228
  function isPathInput(x) {
271
229
  return typeof x === "object" && x !== null && typeof x.path === "string";
272
230
  }
231
+ function isNamedFileInput(x) {
232
+ if (typeof x !== "object" || x === null) return false;
233
+ const value = x;
234
+ return typeof value.filename === "string" && (value.data instanceof Uint8Array || isReadable(value.data));
235
+ }
273
236
  async function streamToBuffer(stream) {
274
237
  const chunks = [];
275
238
  for await (const chunk of stream) chunks.push(Buffer.from(chunk));
276
239
  return Buffer.concat(chunks);
277
240
  }
278
- async function resolveOne(input, fallbackName) {
241
+ function toBlobBytes(bytes) {
242
+ return new Uint8Array(bytes);
243
+ }
244
+ async function resolveOne(input) {
279
245
  if (typeof input === "string" || isPathInput(input)) {
280
246
  const path = typeof input === "string" ? input : input.path;
281
247
  const data = await readFile(path);
282
248
  const filename = basename(path);
283
- return { data: new Blob([data]), filename, contentType: contentTypeFor(filename) };
284
- }
285
- if (input instanceof Uint8Array) {
286
- return {
287
- data: new Blob([input]),
288
- filename: fallbackName,
289
- contentType: contentTypeFor(fallbackName)
290
- };
249
+ return { data: new Blob([toBlobBytes(data)]), filename, contentType: contentTypeFor(filename) };
291
250
  }
292
- if (isReadable(input)) {
293
- const buf = await streamToBuffer(input);
251
+ if (isNamedFileInput(input)) {
252
+ const bytes = input.data instanceof Uint8Array ? input.data : await streamToBuffer(input.data);
294
253
  return {
295
- data: new Blob([buf]),
296
- filename: fallbackName,
297
- contentType: contentTypeFor(fallbackName)
254
+ data: new Blob([toBlobBytes(bytes)]),
255
+ filename: input.filename,
256
+ contentType: input.contentType ?? contentTypeFor(input.filename)
298
257
  };
299
258
  }
300
259
  throw new Error("Unsupported file input type");
301
260
  }
302
261
  async function resolveInputs(input) {
303
- const list = input.files ?? (input.file !== void 0 ? [input.file] : []);
304
- if (list.length === 0) throw new Error("runFlow requires `file` or `files`");
305
- const fallback = input.fileName ?? "upload.bin";
306
- return Promise.all(list.map((item) => resolveOne(item, fallback)));
262
+ const list = input.files;
263
+ if (list.length === 0) throw new Error("runFlow requires `files`");
264
+ return Promise.all(list.map((item) => resolveOne(item)));
307
265
  }
308
266
 
309
- // src/api/upload.ts
310
- async function uploadFiles(http, parts, signal) {
267
+ // src/sdk/api/run-flow.ts
268
+ async function runFlow(http, flowId, parts, signal, useDraftVersion = false, context, callbackUrl, language) {
311
269
  const form = new FormData();
312
270
  for (const part of parts) {
313
271
  form.append("files", new File([part.data], part.filename, { type: part.contentType }));
314
272
  }
315
- const res = await http.requestJson("POST", "/upload", { body: form, signal });
316
- return [res.id];
273
+ if (context !== void 0) form.append("json_context", JSON.stringify(context));
274
+ if (callbackUrl !== void 0) form.append("callback_url", callbackUrl);
275
+ if (language !== void 0) form.append("language", language);
276
+ if (useDraftVersion) form.append("useDraftVersion", "true");
277
+ return http.requestJson(
278
+ "POST",
279
+ `/flows/${flowId}/execute-async`,
280
+ { body: form, signal }
281
+ );
282
+ }
283
+
284
+ // src/sdk/api/execution-status.ts
285
+ async function getStatus(http, executionResultId, signal) {
286
+ const res = await http.requestRaw("GET", `/executions/${executionResultId}/status`, { signal });
287
+ const text = await res.text();
288
+ const body = text ? JSON.parse(text) : { status: "" };
289
+ const pollAfterMs = parsePositiveInt(res.headers.get("x-poll-after"));
290
+ return pollAfterMs === void 0 ? body : { ...body, pollAfterMs };
291
+ }
292
+ function parsePositiveInt(value) {
293
+ if (!value) return void 0;
294
+ const parsed = Number(value);
295
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : void 0;
317
296
  }
318
297
 
319
- // src/api/run-flow.ts
320
- async function runFlow(http, flowId, uuidFiles, signal, once = false, context, callbackUrl) {
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
- if (callbackUrl !== void 0) body.callback_url = callbackUrl;
325
- const res = await http.requestJson("POST", path, {
326
- query: { async_mode: "true" },
327
- headers: { "content-type": "application/json" },
328
- body: JSON.stringify(body),
298
+ // src/sdk/api/execution-result.ts
299
+ async function getResult(http, executionResultId, signal, language) {
300
+ return http.requestJson("GET", `/executions/${executionResultId}/result`, {
301
+ query: language ? { language } : void 0,
329
302
  signal
330
303
  });
331
- return res.execution_result_id;
332
304
  }
333
305
 
334
- // src/api/execution-status.ts
335
- async function getStatus(http, executionResultId, signal) {
336
- return http.requestJson("GET", `/flows/execution-status/${executionResultId}`, { signal });
306
+ // src/sdk/api/list-flows.ts
307
+ async function listFlows(http) {
308
+ const flows = await http.requestJson("GET", "/flows");
309
+ return Array.isArray(flows) ? flows.map(toFlowListItem) : [];
310
+ }
311
+ function toFlowListItem(flow) {
312
+ const value = flow && typeof flow === "object" ? flow : {};
313
+ return {
314
+ id: toStringValue(value.id),
315
+ name: toStringValue(value.name),
316
+ state: toStateValue(value.state)
317
+ };
318
+ }
319
+ function toStringValue(value) {
320
+ return typeof value === "string" ? value : "";
321
+ }
322
+ function toStateValue(value) {
323
+ return typeof value === "string" && value.trim() ? value : "unknown";
324
+ }
325
+
326
+ // src/sdk/api/list-organizations.ts
327
+ async function listOrganizations(http) {
328
+ const organizations = await http.requestJson("GET", "/organizations");
329
+ return Array.isArray(organizations) ? organizations.map(toAccessibleOrganization).filter(isAccessibleOrganization) : [];
330
+ }
331
+ function toAccessibleOrganization(organization) {
332
+ const value = organization && typeof organization === "object" ? organization : {};
333
+ const id = toStringValue2(value.id).trim();
334
+ if (!id) return void 0;
335
+ const name = toStringValue2(value.name).trim() || id;
336
+ return {
337
+ id,
338
+ name
339
+ };
340
+ }
341
+ function isAccessibleOrganization(organization) {
342
+ return organization !== void 0;
343
+ }
344
+ function toStringValue2(value) {
345
+ return typeof value === "string" ? value : "";
346
+ }
347
+
348
+ // src/sdk/api/flow-metadata.ts
349
+ async function getFlowMetadata(http, flowId) {
350
+ return http.requestJson("GET", `/flows/${encodeURIComponent(flowId)}/metadata`);
337
351
  }
338
352
 
339
- // src/api/execution-result.ts
340
- async function getResult(http, executionResultId, signal) {
341
- return http.requestJson("GET", `/flows/execution-result/${executionResultId}`, { signal });
353
+ // src/sdk/api/flow-definition.ts
354
+ async function getFlowDefinition(http, flowId) {
355
+ return http.requestJson("GET", `/flows/${encodeURIComponent(flowId)}/definition`);
356
+ }
357
+
358
+ // src/sdk/api/organization-documents.ts
359
+ async function listOrganizationDocuments(http, options = {}) {
360
+ const result = await http.requestJson("GET", "/organization-documents", {
361
+ query: options.prefix ? { prefix: options.prefix } : void 0
362
+ });
363
+ const value = result && typeof result === "object" ? result : {};
364
+ return {
365
+ configured: value.configured === true,
366
+ documents: Array.isArray(value.documents) ? value.documents.map(toOrganizationDocument).filter(isOrganizationDocument) : []
367
+ };
368
+ }
369
+ async function downloadOrganizationDocument(http, path) {
370
+ const result = await http.requestBytes("GET", "/organization-documents/download", {
371
+ query: { path }
372
+ });
373
+ return {
374
+ bytes: result.bytes,
375
+ filename: filenameFromHeaders(result.headers) ?? filenameFromPath(path),
376
+ contentType: result.headers.get("content-type") || void 0
377
+ };
378
+ }
379
+ async function downloadOrganizationDocuments(http, paths) {
380
+ return Promise.all(paths.map((path) => downloadOrganizationDocument(http, path)));
381
+ }
382
+ function toOrganizationDocument(document) {
383
+ const value = document && typeof document === "object" ? document : {};
384
+ const path = toStringValue3(value.path).trim();
385
+ if (!path) return void 0;
386
+ const contentType = toStringValue3(value.contentType).trim();
387
+ return {
388
+ path,
389
+ name: toStringValue3(value.name).trim() || filenameFromPath(path),
390
+ size: toNumberValue(value.size),
391
+ lastModified: toStringValue3(value.lastModified).trim(),
392
+ ...contentType ? { contentType } : {}
393
+ };
394
+ }
395
+ function isOrganizationDocument(document) {
396
+ return document !== void 0;
397
+ }
398
+ function filenameFromHeaders(headers) {
399
+ const disposition = headers.get("content-disposition");
400
+ if (!disposition) return void 0;
401
+ const utf8 = /filename\*=UTF-8''([^;]+)/i.exec(disposition);
402
+ if (utf8?.[1]) return decodeURIComponent(utf8[1]);
403
+ const quoted = /filename="([^"]+)"/i.exec(disposition);
404
+ if (quoted?.[1]) return quoted[1];
405
+ const plain = /filename=([^;]+)/i.exec(disposition);
406
+ return plain?.[1]?.trim();
407
+ }
408
+ function filenameFromPath(path) {
409
+ return path.replace(/\\/g, "/").split("/").filter(Boolean).pop() || path;
410
+ }
411
+ function toStringValue3(value) {
412
+ return typeof value === "string" ? value : "";
413
+ }
414
+ function toNumberValue(value) {
415
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
342
416
  }
343
417
 
344
- // src/polling/poll.ts
418
+ // src/sdk/polling/poll.ts
345
419
  var TERMINAL = /* @__PURE__ */ new Set(["Completed", "Failed"]);
346
420
  var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
347
421
  async function pollUntilTerminal(opts) {
348
422
  const sleep = opts.sleep ?? defaultSleep;
349
423
  const start = Date.now();
350
424
  const elapsed = opts.elapsed ?? (() => Date.now() - start);
425
+ const random = opts.random ?? Math.random;
426
+ let fallbackAttempt = 0;
351
427
  for (; ; ) {
352
428
  if (opts.signal?.aborted) throw new DocmanaAbortError("Polling aborted");
353
- const status = await opts.check();
429
+ let result;
430
+ try {
431
+ result = normalizePollResult(await opts.check());
432
+ } catch (err) {
433
+ if (!(err instanceof DocmanaRateLimitError)) throw err;
434
+ if (elapsed() >= opts.timeoutMs) {
435
+ throw new DocmanaTimeoutError(`Execution did not finish within ${opts.timeoutMs}ms`);
436
+ }
437
+ await sleep(err.retryAfterMs ?? opts.intervalMs);
438
+ continue;
439
+ }
440
+ const { status } = result;
354
441
  if (TERMINAL.has(status)) return status;
355
442
  if (elapsed() >= opts.timeoutMs) {
356
443
  throw new DocmanaTimeoutError(`Execution did not finish within ${opts.timeoutMs}ms`);
357
444
  }
358
- await sleep(opts.intervalMs);
445
+ const fallbackDelay = fullJitterDelay(
446
+ opts.intervalMs,
447
+ opts.maxIntervalMs,
448
+ fallbackAttempt,
449
+ random
450
+ );
451
+ fallbackAttempt += 1;
452
+ await sleep(result.nextDelayMs ?? fallbackDelay);
359
453
  }
360
454
  }
361
-
362
- // src/mappers/mapper.ts
363
- function extractNodeFields(rawNode, targetLanguage) {
364
- const metadataFields = /* @__PURE__ */ new Set([
365
- "node_id",
366
- "node_name",
367
- "node_type",
368
- "feature_name",
369
- "running_time",
370
- "status",
371
- "score",
372
- "feedback",
373
- "translations",
374
- "tokens",
375
- "input_tokens",
376
- "output_tokens",
377
- "errors",
378
- "mana",
379
- "model",
380
- "tool_calls",
381
- "prompt_id",
382
- "prompt",
383
- "prompt_name",
384
- "label",
385
- "scoreDescription",
386
- "score_description",
387
- "result",
388
- "documentType"
389
- ]);
390
- let sourceObj = rawNode;
391
- if (targetLanguage) {
392
- const translations = rawNode.translations;
393
- if (!translations || !translations[targetLanguage]) {
394
- throw new DocmanaError(
395
- `Translation for language '${targetLanguage}' is missing on node '${rawNode.node_name || rawNode.node_id}'`,
396
- { code: "translation_missing", status: 400 }
397
- );
398
- }
399
- sourceObj = translations[targetLanguage];
400
- }
401
- const dynamicFields = {};
402
- for (const [key, value] of Object.entries(sourceObj)) {
403
- if (!metadataFields.has(key)) {
404
- dynamicFields[key] = value;
405
- }
406
- }
407
- const sdkNode = {
408
- status: rawNode.status || "Unknown"
409
- };
410
- if (rawNode.errors && rawNode.errors.length > 0) {
411
- sdkNode.errors = rawNode.errors;
412
- }
413
- const nodeType = rawNode.node_type || "";
414
- const isScoreNode = /^(Validation|Classification|CrossValidation|Conclusion|Finish)$/i.test(
415
- nodeType
416
- );
417
- if (isScoreNode && rawNode.score !== void 0 && rawNode.score !== null) {
418
- sdkNode.score = rawNode.score;
419
- }
420
- const feedback = sourceObj.feedback ?? rawNode.feedback;
421
- if (feedback !== void 0 && feedback !== null) {
422
- sdkNode.feedback = feedback;
423
- }
424
- if (Object.keys(dynamicFields).length > 0) {
425
- sdkNode.data = dynamicFields;
426
- }
427
- return sdkNode;
455
+ function normalizePollResult(result) {
456
+ return typeof result === "string" ? { status: result } : result;
428
457
  }
429
- function mapExecutionResult(raw, targetLanguage) {
430
- const sdkResult = {
431
- status: raw.status || "Unknown",
432
- executionId: String(raw.execution_id || raw.executionResultId || raw.execution_result_id || ""),
433
- documents: {
434
- mappings: []
435
- }
436
- };
437
- if (raw.errors && raw.errors.length > 0) {
438
- sdkResult.errors = raw.errors;
439
- }
440
- const results = raw.results || [];
441
- for (const item of results) {
442
- const isDocSpecific = !!item.document;
443
- if (isDocSpecific && item.document) {
444
- const docReq = item.document;
445
- const docKey = docReq.doc_key || "unknown_doc";
446
- const docName = docReq.name || "";
447
- sdkResult.documents.mappings.push({
448
- name: docName,
449
- reference: docKey
450
- });
451
- const docResult = {
452
- status: item.status || docReq.status || "Unknown",
453
- extractions: {},
454
- validations: {}
455
- };
456
- const nodeResults = item.node_results || [];
457
- for (const node of nodeResults) {
458
- const nodeType = node.node_type || "";
459
- const nodeName = node.node_name || node.node_id || "unknown_node";
460
- if (/^Classification$/i.test(nodeType)) {
461
- const mappedNode = extractNodeFields(node, targetLanguage);
462
- if (!docResult.classifications) {
463
- docResult.classifications = {};
464
- }
465
- docResult.classifications[nodeName] = mappedNode;
466
- } else if (/^MetadataExtraction$/i.test(nodeType)) {
467
- docResult.metadataExtraction = extractNodeFields(node, targetLanguage);
468
- } else if (/^Extraction$/i.test(nodeType)) {
469
- if (docResult.extractions) {
470
- docResult.extractions[nodeName] = extractNodeFields(node, targetLanguage);
471
- }
472
- } else if (/^Validation$/i.test(nodeType)) {
473
- if (docResult.validations) {
474
- docResult.validations[nodeName] = extractNodeFields(node, targetLanguage);
475
- }
476
- }
477
- }
478
- if (docResult.extractions && Object.keys(docResult.extractions).length === 0) {
479
- delete docResult.extractions;
480
- }
481
- if (docResult.validations && Object.keys(docResult.validations).length === 0) {
482
- delete docResult.validations;
483
- }
484
- sdkResult.documents[docKey] = docResult;
485
- } else {
486
- const nodeResults = item.node_results || [];
487
- for (const node of nodeResults) {
488
- const nodeType = node.node_type || "";
489
- const nodeName = node.node_name || node.node_id || "unknown_node";
490
- if (/^CrossValidation$/i.test(nodeType)) {
491
- if (!sdkResult.crossValidations) {
492
- sdkResult.crossValidations = {};
493
- }
494
- sdkResult.crossValidations[nodeName] = extractNodeFields(
495
- node,
496
- targetLanguage
497
- );
498
- } else if (/^(Conclusion|Finish)$/i.test(nodeType)) {
499
- if (!sdkResult.conclusions) {
500
- sdkResult.conclusions = {};
501
- }
502
- sdkResult.conclusions[nodeName] = extractNodeFields(
503
- node,
504
- targetLanguage
505
- );
506
- }
507
- }
508
- }
509
- }
510
- return sdkResult;
458
+ function fullJitterDelay(intervalMs, maxIntervalMs, attempt, random) {
459
+ const capDelay = Math.min(maxIntervalMs, intervalMs * 2 ** attempt);
460
+ return Math.round(intervalMs + random() * (capDelay - intervalMs));
511
461
  }
512
462
 
513
- // src/client.ts
463
+ // src/sdk/client.ts
514
464
  var Docmana = class {
515
465
  config;
516
466
  http;
517
467
  constructor(config) {
518
468
  this.config = resolveConfig(config);
519
- const tokenManager = new TokenManager({
520
- clientId: this.config.clientId,
521
- clientSecret: this.config.clientSecret,
522
- tokenEndpoint: this.config.tokenEndpoint,
523
- scope: this.config.scope,
524
- fetchImpl: this.config.fetchImpl,
525
- tokenCache: this.config.tokenCache
526
- });
527
469
  this.http = new HttpClient({
528
470
  apiBaseUrl: this.config.apiBaseUrl,
529
471
  fetchImpl: this.config.fetchImpl,
530
- tokenManager,
472
+ accessToken: this.config.accessToken,
473
+ getAccessToken: this.config.getAccessToken,
531
474
  headers: this.config.headers
532
475
  });
533
476
  }
534
- async runFlowAsync(flowId, input) {
535
- const parts = await resolveInputs(input);
536
- const fileIds = await uploadFiles(this.http, parts, input.signal);
537
- const executionResultId = await runFlow(
538
- this.http,
539
- flowId,
540
- fileIds,
541
- input.signal,
542
- input.once ?? false,
543
- input.context
544
- );
545
- return { executionResultId, fileIds };
546
- }
547
- async runFlowWithCallback(flowId, input) {
548
- const parts = await resolveInputs(input);
549
- const fileIds = await uploadFiles(this.http, parts, input.signal);
550
- const executionResultId = await runFlow(
477
+ async runFlowAsync(flowId, params, options = {}) {
478
+ const parts = await resolveInputs(params);
479
+ return runFlow(
551
480
  this.http,
552
481
  flowId,
553
- fileIds,
554
- input.signal,
555
- input.once ?? false,
556
- input.context,
557
- input.callbackUrl
482
+ parts,
483
+ options.signal,
484
+ params.useDraftVersion ?? false,
485
+ params.context,
486
+ params.callbackUrl,
487
+ params.language
558
488
  );
559
- return { executionResultId, fileIds };
560
489
  }
561
490
  async getStatus(executionResultId, signal) {
562
491
  return getStatus(this.http, executionResultId, signal);
563
492
  }
564
493
  async getResult(executionResultId, signal, language) {
565
- const rawResult = await getResult(this.http, executionResultId, signal);
566
- return mapExecutionResult(rawResult, language);
494
+ return getResult(this.http, executionResultId, signal, language);
567
495
  }
568
- async runFlow(flowId, input) {
569
- const { executionResultId } = await this.runFlowAsync(flowId, input);
496
+ async runFlow(flowId, params, options = {}) {
497
+ const { executionResultId } = await this.runFlowAsync(flowId, params, {
498
+ signal: options.signal
499
+ });
500
+ let attempt = 0;
501
+ const startedAt = Date.now();
570
502
  await pollUntilTerminal({
571
- check: async () => (await this.getStatus(executionResultId, input.signal)).status,
572
- intervalMs: input.pollIntervalMs ?? this.config.pollIntervalMs,
573
- timeoutMs: input.timeoutMs ?? this.config.timeoutMs,
574
- signal: input.signal
503
+ check: async () => {
504
+ attempt += 1;
505
+ try {
506
+ const statusResult = await this.getStatus(executionResultId, options.signal);
507
+ const status = statusResult.status;
508
+ const nextPollAfterMs = this.isTerminalStatus(status) ? void 0 : statusResult.pollAfterMs;
509
+ options.onPoll?.({
510
+ executionResultId,
511
+ status,
512
+ attempt,
513
+ elapsedMs: Date.now() - startedAt,
514
+ nextPollAfterMs
515
+ });
516
+ return { status, nextDelayMs: nextPollAfterMs };
517
+ } catch (err) {
518
+ if (err instanceof DocmanaRateLimitError) {
519
+ options.onRateLimit?.({
520
+ executionResultId,
521
+ attempt,
522
+ elapsedMs: Date.now() - startedAt,
523
+ retryAfterMs: err.retryAfterMs ?? options.pollIntervalMs ?? this.config.pollIntervalMs
524
+ });
525
+ }
526
+ throw err;
527
+ }
528
+ },
529
+ intervalMs: options.pollIntervalMs ?? this.config.pollIntervalMs,
530
+ maxIntervalMs: options.pollMaxIntervalMs ?? this.config.pollMaxIntervalMs,
531
+ timeoutMs: options.timeoutMs ?? this.config.timeoutMs,
532
+ signal: options.signal
575
533
  });
576
- const result = await this.getResult(executionResultId, input.signal, input.language);
534
+ const result = await this.getResult(executionResultId, options.signal, params.language);
577
535
  if (result.status === "Failed") {
578
536
  throw new DocmanaExecutionError(
579
537
  `Flow ${flowId} execution failed`,
@@ -583,9 +541,33 @@ var Docmana = class {
583
541
  }
584
542
  return result;
585
543
  }
544
+ async listFlows() {
545
+ return listFlows(this.http);
546
+ }
547
+ async listOrganizations() {
548
+ return listOrganizations(this.http);
549
+ }
550
+ async getFlowMetadata(flowId) {
551
+ return getFlowMetadata(this.http, flowId);
552
+ }
553
+ async getFlowDefinition(flowId) {
554
+ return getFlowDefinition(this.http, flowId);
555
+ }
556
+ async listOrganizationDocuments(options = {}) {
557
+ return listOrganizationDocuments(this.http, options);
558
+ }
559
+ async downloadOrganizationDocument(path) {
560
+ return downloadOrganizationDocument(this.http, path);
561
+ }
562
+ async downloadOrganizationDocuments(paths) {
563
+ return downloadOrganizationDocuments(this.http, paths);
564
+ }
565
+ isTerminalStatus(status) {
566
+ return status === "Completed" || status === "Failed";
567
+ }
586
568
  };
587
569
 
588
- // src/cli.ts
570
+ // src/cli/errors.ts
589
571
  var CliUsageError = class extends Error {
590
572
  constructor(message) {
591
573
  super(message);
@@ -601,226 +583,212 @@ var CliSilentError = class extends Error {
601
583
  Object.setPrototypeOf(this, new.target.prototype);
602
584
  }
603
585
  };
604
- var HELP = {
605
- root: "Run Docmana document-analysis flows.",
606
- flow: "Manage and run Docmana flows.",
607
- config: "Manage Docmana CLI configuration.",
608
- configInit: "Create or update a local Docmana CLI config file.",
609
- login: "Acquire and cache a Docmana OAuth2 access token.",
610
- run: "Upload documents, run a Docmana flow, wait for completion, and print the result."
611
- };
612
- var DEFAULT_CONFIG_FILE = "docmana.config.json";
613
- var DEFAULT_TOKEN_CACHE_FILE = "docmana.token.json";
614
- async function runCli(argv = process.argv.slice(2), env = process.env, io = {
615
- stdout: (text) => process.stdout.write(text),
616
- stderr: (text) => process.stderr.write(text)
617
- }, clientFactory = (config) => new Docmana(config), deps = {}) {
618
- const program = buildProgram(env, io, clientFactory, deps);
619
- if (argv.length === 0) {
620
- io.stdout(program.helpInformation());
621
- return 0;
586
+
587
+ // src/cli/formatting.ts
588
+ function formatFlowList(flows) {
589
+ if (flows.length === 0) return "No flows found.\n";
590
+ const lines = ["Docmana flows"];
591
+ for (const flow of flows) {
592
+ const id = flow.id || "(no id)";
593
+ const name = flow.name || "(no name)";
594
+ const state = flow.state ? ` [${flow.state}]` : "";
595
+ lines.push(`- ${id} - ${name}${state}`);
622
596
  }
623
- try {
624
- await program.parseAsync(argv, { from: "user" });
625
- return 0;
626
- } catch (err) {
627
- if (err instanceof CliSilentError) {
628
- return err.exitCode;
629
- }
630
- if (err instanceof CliUsageError) {
631
- io.stderr(`Error: ${err.message}
632
- `);
633
- return 2;
634
- }
635
- if (err instanceof CommanderError) {
636
- return err.exitCode === 0 ? 0 : 2;
637
- }
638
- io.stderr(`${formatRuntimeError(err)}
639
- `);
640
- return 1;
597
+ return `${lines.join("\n")}
598
+ `;
599
+ }
600
+ function formatOrganizationList(organizations) {
601
+ if (organizations.length === 0) return "No organizations found.\n";
602
+ const lines = ["Docmana organizations"];
603
+ for (const organization of organizations) {
604
+ const id = organization.id || "(no id)";
605
+ const name = organization.name || "(no name)";
606
+ lines.push(`- ${id} - ${name}`);
641
607
  }
608
+ return `${lines.join("\n")}
609
+ `;
642
610
  }
643
- function buildProgram(env, io, clientFactory, deps) {
644
- const program = new Command();
645
- program.name("docmana").description(HELP.root).showHelpAfterError().configureOutput({
646
- writeOut: io.stdout,
647
- writeErr: io.stderr,
648
- outputError: (str, write) => write(str)
649
- }).exitOverride();
650
- program.command("login").description(HELP.login).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("--token-url <url>", "OAuth2 token endpoint; defaults to DOCMANA_TOKEN_URL").option("--scope <scope>", "OAuth2 scope; defaults to DOCMANA_SCOPE").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).action(async (options) => {
651
- await loginCommand(options, env, io, deps);
652
- });
653
- const flow = program.command("flow").description(HELP.flow).action(() => {
654
- io.stdout(flow.helpInformation());
655
- });
656
- 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) => {
657
- await runFlowCommand(flowId, options, env, io, clientFactory, deps);
658
- });
659
- const config = program.command("config").description(HELP.config).action(() => {
660
- io.stdout(config.helpInformation());
661
- });
662
- config.command("init").description(HELP.configInit).option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).action(async (options) => {
663
- await configInitCommand(options, io, deps);
664
- });
665
- return program;
611
+ function formatOrganizationDocumentList(documents) {
612
+ if (documents.length === 0) return "No organization documents found.\n";
613
+ const lines = ["Docmana organization documents"];
614
+ for (const document of documents) {
615
+ const contentType = document.contentType ? ` [${document.contentType}]` : "";
616
+ lines.push(`- ${document.path} - ${document.name} (${document.size} bytes)${contentType}`);
617
+ }
618
+ return `${lines.join("\n")}
619
+ `;
666
620
  }
667
- async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
668
- try {
669
- const files = parseFiles(options.files);
670
- const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
671
- const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
672
- const clientSecret = firstNonEmpty(
673
- options.clientSecret,
674
- env.DOCMANA_CLIENT_SECRET,
675
- cliConfig.clientSecret
676
- );
677
- const organisation = firstNonEmpty(
678
- options.organisation,
679
- env.DOCMANA_ORGANISATION,
680
- cliConfig.organisation
681
- );
682
- const apiBaseUrl = firstNonEmpty(options.apiUrl, env.DOCMANA_API_URL, cliConfig.apiBaseUrl);
683
- const tokenEndpoint = firstNonEmpty(
684
- options.tokenUrl,
685
- env.DOCMANA_TOKEN_URL,
686
- cliConfig.tokenEndpoint
687
- );
688
- const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope);
689
- if (!clientId)
690
- throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
691
- if (!clientSecret) {
692
- throw new CliUsageError(
693
- "Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET."
694
- );
695
- }
696
- if (!organisation) {
697
- throw new CliUsageError("Missing organisation. Use --organisation or DOCMANA_ORGANISATION.");
621
+ function formatHumanResult(result) {
622
+ const lines = ["Docmana flow completed", `Status: ${String(result.status ?? "Unknown")}`];
623
+ const executionId = result.executionId;
624
+ if (executionId) lines.push(`Execution id: ${String(executionId)}`);
625
+ if (result.documents && result.documents.mappings) {
626
+ lines.push(`Results: ${result.documents.mappings.length}`);
627
+ }
628
+ if (result.errors && result.errors.length > 0) {
629
+ lines.push(`Errors: ${result.errors.length}`);
630
+ }
631
+ const previewData = {};
632
+ if (result.documents && result.documents.mappings) {
633
+ for (const mapping of result.documents.mappings.slice(0, 3)) {
634
+ const docKey = mapping.reference;
635
+ previewData[mapping.name] = result.documents[docKey];
698
636
  }
699
- const client = clientFactory({
700
- clientId,
701
- clientSecret,
702
- apiBaseUrl,
703
- tokenEndpoint,
704
- scope,
705
- headers: { "X-Selected-Organization": organisation },
706
- tokenCache: createFileTokenCache(resolveTokenCachePath(options.tokenCache, getCwd(deps)))
707
- });
708
- const result = await client.runFlow(flowId, {
709
- files: files.map((path) => ({ path })),
710
- language: options.language
711
- });
712
- if (options.json) {
713
- io.stdout(`${JSON.stringify(result, null, 2)}
714
- `);
715
- return;
637
+ }
638
+ const preview = formatResultsPreview(previewData);
639
+ if (preview) lines.push("", "Result preview:", preview);
640
+ lines.push("", "Use --json to print the complete result payload.");
641
+ return `${lines.join("\n")}
642
+ `;
643
+ }
644
+ function formatRuntimeError(err) {
645
+ return formatHumanError(err);
646
+ }
647
+ function formatHumanError(err) {
648
+ if (err instanceof DocmanaExecutionError) {
649
+ const lines = [`Error: ${err.message}`];
650
+ if (err.errors && err.errors.length > 0) {
651
+ lines.push("Global errors:");
652
+ for (const e of err.errors) {
653
+ lines.push(` - ${formatSingleError(e)}`);
654
+ }
716
655
  }
717
- io.stdout(formatHumanResult(result));
718
- } catch (err) {
719
- if (options.json) {
720
- if (err instanceof DocmanaExecutionError && err.result) {
721
- io.stdout(`${JSON.stringify(err.result, null, 2)}
722
- `);
723
- } else {
724
- const errorPayload = {
725
- error: err instanceof Error ? err.message : String(err)
726
- };
727
- if (err instanceof DocmanaError) {
728
- errorPayload.code = err.code;
729
- if (err.status !== void 0) errorPayload.status = err.status;
730
- if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
656
+ const result = err.result;
657
+ if (result?.documents?.mappings && result.documents.mappings.length > 0) {
658
+ lines.push("Document execution results:");
659
+ for (const mapping of result.documents.mappings) {
660
+ const docKey = mapping.reference;
661
+ const docName = mapping.name;
662
+ const docResult = result.documents[docKey];
663
+ if (!docResult) {
664
+ lines.push(` - Document: ${docName} (Status: Unknown)`);
665
+ continue;
666
+ }
667
+ const status = docResult.status || "Unknown";
668
+ const nodes = [];
669
+ if (docResult.classifications) nodes.push(...Object.values(docResult.classifications));
670
+ if (docResult.metadataExtraction) nodes.push(docResult.metadataExtraction);
671
+ if (docResult.extractions) nodes.push(...Object.values(docResult.extractions));
672
+ if (docResult.validations) nodes.push(...Object.values(docResult.validations));
673
+ const hasNodeFailures = nodes.some(
674
+ (n) => n.status === "Failed" || n.errors && n.errors.length > 0
675
+ );
676
+ lines.push(` - Document: ${docName} (Status: ${status})`);
677
+ if (docResult.status !== "Failed" && !hasNodeFailures) continue;
678
+ if (docResult.classifications) {
679
+ for (const [nodeName, node] of Object.entries(docResult.classifications)) {
680
+ if (node.errors && node.errors.length > 0) {
681
+ lines.push(` Classification: ${nodeName} (Status: ${node.status})`);
682
+ for (const nodeErr of node.errors) lines.push(` - ${nodeErr}`);
683
+ }
684
+ }
685
+ }
686
+ if (docResult.metadataExtraction?.errors?.length) {
687
+ lines.push(` MetadataExtraction: (Status: ${docResult.metadataExtraction.status})`);
688
+ for (const nodeErr of docResult.metadataExtraction.errors) {
689
+ lines.push(` - ${nodeErr}`);
690
+ }
691
+ }
692
+ if (docResult.extractions) {
693
+ for (const [nodeName, node] of Object.entries(docResult.extractions)) {
694
+ if (node.status === "Failed" || node.errors && node.errors.length > 0) {
695
+ lines.push(` Extraction: ${nodeName} (Status: ${node.status})`);
696
+ if (node.errors) {
697
+ for (const nodeErr of node.errors) lines.push(` - ${nodeErr}`);
698
+ }
699
+ }
700
+ }
701
+ }
702
+ if (docResult.validations) {
703
+ for (const [nodeName, node] of Object.entries(docResult.validations)) {
704
+ if (node.status === "Failed" || node.errors && node.errors.length > 0) {
705
+ lines.push(` Validation: ${nodeName} (Status: ${node.status})`);
706
+ if (node.errors) {
707
+ for (const nodeErr of node.errors) lines.push(` - ${nodeErr}`);
708
+ }
709
+ }
710
+ }
731
711
  }
732
- io.stdout(`${JSON.stringify(errorPayload, null, 2)}
733
- `);
734
712
  }
735
- throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
736
713
  }
737
- throw err;
714
+ return lines.join("\n");
738
715
  }
716
+ if (err instanceof DocmanaError) {
717
+ return `Error: ${err.message} (Code: ${err.code})`;
718
+ }
719
+ if (err instanceof Error) {
720
+ return `Error: ${err.message}`;
721
+ }
722
+ return `Error: ${String(err)}`;
739
723
  }
740
- async function loginCommand(options, env, io, deps) {
741
- const auth = await resolveAuthConfig(options, env, deps);
742
- const tokenCachePath = resolveTokenCachePath(options.tokenCache, getCwd(deps));
743
- const tokenCache = createFileTokenCache(tokenCachePath);
744
- const tokenManager = new TokenManager({
745
- clientId: auth.clientId,
746
- clientSecret: auth.clientSecret,
747
- tokenEndpoint: auth.tokenEndpoint,
748
- scope: auth.scope,
749
- fetchImpl: deps.fetch ?? fetch,
750
- tokenCache
751
- });
752
- await tokenManager.getToken();
753
- const cached = await tokenCache.read();
754
- io.stdout(`Logged in. Token cached at ${tokenCachePath}
755
- `);
756
- if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
757
- }
758
- function formatTokenExpiry(expiresAt) {
759
- const expiresAtDate = new Date(expiresAt);
760
- return [
761
- `Token expires at local time ${expiresAtDate.toLocaleString()}`,
762
- `Token expires at UTC ${expiresAtDate.toISOString()}`
763
- ].join("\n") + "\n";
724
+ function formatResultsPreview(results) {
725
+ if (!results) return void 0;
726
+ const preview = JSON.stringify(results, null, 2);
727
+ if (!preview) return void 0;
728
+ return preview.length > 2e3 ? `${preview.slice(0, 2e3)}
729
+ ...` : preview;
764
730
  }
765
- async function resolveAuthConfig(options, env, deps) {
766
- const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
767
- const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
768
- const clientSecret = firstNonEmpty(
769
- options.clientSecret,
770
- env.DOCMANA_CLIENT_SECRET,
771
- cliConfig.clientSecret
772
- );
773
- const tokenEndpoint = firstNonEmpty(options.tokenUrl, env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint;
774
- const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope;
775
- if (!clientId)
776
- throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
777
- if (!clientSecret) {
778
- throw new CliUsageError("Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET.");
731
+ function formatSingleError(e) {
732
+ if (typeof e === "string") return e;
733
+ if (e && typeof e === "object") {
734
+ const obj = e;
735
+ if (typeof obj.msg === "string") return obj.msg;
736
+ if (typeof obj.message === "string") return obj.message;
737
+ if (typeof obj.error === "string") return obj.error;
738
+ try {
739
+ return JSON.stringify(e);
740
+ } catch {
741
+ return String(e);
742
+ }
779
743
  }
780
- return { clientId, clientSecret, tokenEndpoint, scope };
744
+ return String(e);
781
745
  }
782
- async function configInitCommand(options, io, deps) {
783
- const cwd = getCwd(deps);
784
- const configPath = resolveConfigPath(options.config, cwd);
785
- const existing = await loadCliConfig(options.config, cwd, false);
786
- const defaultPrompt = deps.prompt ? void 0 : createDefaultPrompt();
787
- const prompt = deps.prompt ?? defaultPrompt?.prompt;
788
- if (!prompt) throw new CliUsageError("Unable to initialize interactive prompt");
789
- try {
790
- const config = {
791
- apiBaseUrl: await prompt("API base URL", {
792
- defaultValue: existing.apiBaseUrl ?? DEFAULTS.apiBaseUrl
793
- }),
794
- tokenEndpoint: await prompt("OAuth2 token endpoint", {
795
- defaultValue: existing.tokenEndpoint ?? DEFAULTS.tokenEndpoint
796
- }),
797
- scope: await prompt("OAuth2 scope", {
798
- defaultValue: existing.scope ?? DEFAULTS.scope
799
- }),
800
- organisation: await prompt("Organisation", {
801
- defaultValue: existing.organisation
802
- }),
803
- clientId: await prompt("Client id", {
804
- defaultValue: existing.clientId
805
- }),
806
- clientSecret: await prompt("Client secret", {
807
- defaultValue: existing.clientSecret,
808
- secret: true
809
- })
810
- };
811
- await mkdir(dirname(configPath), { recursive: true });
812
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}
813
- `, "utf8");
814
- io.stdout(`Wrote ${configPath}
815
- `);
816
- io.stdout(
817
- `Warning: ${DEFAULT_CONFIG_FILE} contains credentials and should stay ignored by Git.
818
- `
819
- );
820
- } finally {
821
- defaultPrompt?.close();
746
+
747
+ // src/cli/types.ts
748
+ var DEFAULT_CONFIG_FILE = "docmana.config.json";
749
+ var DEFAULT_TOKEN_CACHE_FILE = "docmana.token.json";
750
+ var HELP = {
751
+ root: "Run Docmana document-analysis flows.",
752
+ flow: "Manage and run Docmana flows.",
753
+ organization: "Manage Docmana organizations.",
754
+ organizationDocument: "Manage organization documents.",
755
+ config: "Manage Docmana CLI configuration.",
756
+ configInit: "Create or update a local Docmana CLI config file.",
757
+ login: "Acquire and cache a Docmana OAuth2 access token.",
758
+ run: "Upload documents, run a Docmana flow, wait for completion, and print the result.",
759
+ list: "List Docmana flows available to the Bearer token.",
760
+ metadata: "Print Docmana flow runtime metadata as JSON.",
761
+ definition: "Print Docmana flow definition as JSON.",
762
+ organizationList: "List Docmana organizations available to the Bearer token.",
763
+ organizationDocumentList: "List organization documents available to the Bearer token.",
764
+ organizationDocumentDownload: "Download an organization document."
765
+ };
766
+
767
+ // src/cli/config-store.ts
768
+ import { existsSync } from "fs";
769
+ import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
770
+ import { dirname, resolve } from "path";
771
+ import { createInterface } from "readline/promises";
772
+
773
+ // src/cli/utils.ts
774
+ function getCwd(deps) {
775
+ return deps.cwd ?? process.cwd();
776
+ }
777
+ function firstNonEmpty(...values) {
778
+ return values.map((value) => value?.trim()).find(Boolean);
779
+ }
780
+ function parseFiles(filesCsv) {
781
+ const files = filesCsv?.split(",").map((file) => file.trim()).filter(Boolean) ?? [];
782
+ if (files.length === 0) {
783
+ throw new CliUsageError("Missing files. Use --files <path[,path]>.");
822
784
  }
785
+ return files;
786
+ }
787
+ function formatElapsedSeconds(elapsedMs) {
788
+ return `${Math.floor(elapsedMs / 1e3)}s`;
823
789
  }
790
+
791
+ // src/cli/config-store.ts
824
792
  async function loadCliConfig(configPathOption, cwd, requireExisting) {
825
793
  const configPath = resolveConfigPath(configPathOption, cwd);
826
794
  if (!existsSync(configPath)) {
@@ -848,23 +816,16 @@ function normalizeCliConfig(config) {
848
816
  apiBaseUrl: stringValue(config.apiBaseUrl),
849
817
  tokenEndpoint: stringValue(config.tokenEndpoint),
850
818
  scope: stringValue(config.scope),
851
- organisation: stringValue(config.organisation),
852
819
  clientId: stringValue(config.clientId),
853
820
  clientSecret: stringValue(config.clientSecret)
854
821
  };
855
822
  }
856
- function stringValue(value) {
857
- return typeof value === "string" && value.trim() ? value : void 0;
858
- }
859
823
  function resolveConfigPath(configPathOption, cwd) {
860
824
  return resolve(cwd, configPathOption ?? DEFAULT_CONFIG_FILE);
861
825
  }
862
826
  function resolveTokenCachePath(tokenCachePathOption, cwd) {
863
827
  return resolve(cwd, tokenCachePathOption ?? DEFAULT_TOKEN_CACHE_FILE);
864
828
  }
865
- function getCwd(deps) {
866
- return deps.cwd ?? process.cwd();
867
- }
868
829
  function createFileTokenCache(path) {
869
830
  return {
870
831
  async read() {
@@ -897,6 +858,11 @@ function createFileTokenCache(path) {
897
858
  }
898
859
  };
899
860
  }
861
+ async function writeCliConfig(configPath, config) {
862
+ await mkdir(dirname(configPath), { recursive: true });
863
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
864
+ `, "utf8");
865
+ }
900
866
  function createDefaultPrompt() {
901
867
  if (!process.stdin.isTTY) {
902
868
  let index = 0;
@@ -920,6 +886,9 @@ function createDefaultPrompt() {
920
886
  close: () => rl.close()
921
887
  };
922
888
  }
889
+ function stringValue(value) {
890
+ return typeof value === "string" && value.trim() ? value : void 0;
891
+ }
923
892
  function formatPrompt(label, options) {
924
893
  const defaultHint = options.defaultValue && !options.secret ? ` [${options.defaultValue}]` : options.defaultValue && options.secret ? " [current value hidden]" : "";
925
894
  return `${label}${defaultHint}: `;
@@ -929,157 +898,515 @@ async function readStdinLines() {
929
898
  for await (const chunk of process.stdin) input += String(chunk);
930
899
  return input.split(/\r?\n/);
931
900
  }
932
- function parseFiles(filesCsv) {
933
- const files = filesCsv?.split(",").map((file) => file.trim()).filter(Boolean) ?? [];
934
- if (files.length === 0) throw new CliUsageError("Missing files. Use --files <path[,path]>.");
935
- return files;
936
- }
937
- function firstNonEmpty(...values) {
938
- return values.map((value) => value?.trim()).find(Boolean);
939
- }
940
- function formatHumanResult(result) {
941
- const lines = ["Docmana flow completed", `Status: ${String(result.status ?? "Unknown")}`];
942
- const executionId = result.executionId;
943
- if (executionId) lines.push(`Execution id: ${String(executionId)}`);
944
- if (result.documents && result.documents.mappings) {
945
- lines.push(`Results: ${result.documents.mappings.length}`);
901
+
902
+ // src/cli/commands/config-init.ts
903
+ async function configInitCommand(options, io, deps) {
904
+ const cwd = getCwd(deps);
905
+ const configPath = resolveConfigPath(options.config, cwd);
906
+ const existing = await loadCliConfig(options.config, cwd, false);
907
+ const defaultPrompt = deps.prompt ? void 0 : createDefaultPrompt();
908
+ const prompt = deps.prompt ?? defaultPrompt?.prompt;
909
+ if (!prompt) throw new CliUsageError("Unable to initialize interactive prompt");
910
+ try {
911
+ const config = {
912
+ apiBaseUrl: await prompt("API base URL", {
913
+ defaultValue: existing.apiBaseUrl
914
+ }),
915
+ tokenEndpoint: await prompt("OAuth2 token endpoint", {
916
+ defaultValue: existing.tokenEndpoint ?? DEFAULTS.tokenEndpoint
917
+ }),
918
+ scope: await prompt("OAuth2 scope", {
919
+ defaultValue: existing.scope ?? DEFAULTS.scope
920
+ }),
921
+ clientId: await prompt("Client id", {
922
+ defaultValue: existing.clientId
923
+ }),
924
+ clientSecret: await prompt("Client secret", {
925
+ defaultValue: existing.clientSecret,
926
+ secret: true
927
+ })
928
+ };
929
+ await writeCliConfig(configPath, config);
930
+ io.stdout(`Wrote ${configPath}
931
+ `);
932
+ io.stdout(
933
+ `Warning: ${DEFAULT_CONFIG_FILE} contains credentials and should stay ignored by Git.
934
+ `
935
+ );
936
+ } finally {
937
+ defaultPrompt?.close();
946
938
  }
947
- if (result.errors && result.errors.length > 0) {
948
- lines.push(`Errors: ${result.errors.length}`);
939
+ }
940
+
941
+ // src/cli/auth/token-manager.ts
942
+ var SAFETY_MARGIN_MS = 6e4;
943
+ var TokenManager = class {
944
+ constructor(opts) {
945
+ this.opts = opts;
949
946
  }
950
- const previewData = {};
951
- if (result.documents && result.documents.mappings) {
952
- for (const mapping of result.documents.mappings.slice(0, 3)) {
953
- const docKey = mapping.reference;
954
- previewData[mapping.name] = result.documents[docKey];
947
+ opts;
948
+ token = null;
949
+ expiresAt = 0;
950
+ inflight = null;
951
+ invalidate() {
952
+ this.token = null;
953
+ this.expiresAt = 0;
954
+ void this.opts.tokenCache?.clear().catch(() => void 0);
955
+ }
956
+ async getToken() {
957
+ if (this.token && Date.now() < this.expiresAt) return this.token;
958
+ const cached = await this.readCachedToken();
959
+ if (cached) return cached;
960
+ if (this.inflight) return this.inflight;
961
+ this.inflight = this.acquire().finally(() => {
962
+ this.inflight = null;
963
+ });
964
+ return this.inflight;
965
+ }
966
+ async acquire() {
967
+ const body = new URLSearchParams({
968
+ grant_type: "client_credentials",
969
+ client_id: this.opts.clientId,
970
+ client_secret: this.opts.clientSecret,
971
+ scope: this.opts.scope
972
+ });
973
+ const res = await this.opts.fetchImpl(this.opts.tokenEndpoint, {
974
+ method: "POST",
975
+ headers: { "content-type": "application/x-www-form-urlencoded" },
976
+ body: body.toString()
977
+ });
978
+ if (!res.ok) {
979
+ throw new DocmanaAuthError("Failed to acquire Docmana access token", { status: res.status });
980
+ }
981
+ const json = await res.json();
982
+ if (typeof json.access_token !== "string" || json.access_token.length === 0 || typeof json.expires_in !== "number" || !Number.isFinite(json.expires_in)) {
983
+ throw new DocmanaAuthError("Malformed token response from Docmana CIAM", {
984
+ status: res.status
985
+ });
955
986
  }
987
+ this.token = json.access_token;
988
+ this.expiresAt = Date.now() + json.expires_in * 1e3 - SAFETY_MARGIN_MS;
989
+ await this.writeCachedToken().catch(() => void 0);
990
+ return this.token;
956
991
  }
957
- const preview = formatResultsPreview(previewData);
958
- if (preview) lines.push("", "Result preview:", preview);
959
- lines.push("", "Use --json to print the complete result payload.");
960
- return `${lines.join("\n")}
961
- `;
992
+ async readCachedToken() {
993
+ if (!this.opts.tokenCache) return null;
994
+ let entry;
995
+ try {
996
+ entry = await this.opts.tokenCache.read();
997
+ } catch {
998
+ return null;
999
+ }
1000
+ if (!entry) return null;
1001
+ if (entry.clientId !== this.opts.clientId || entry.tokenEndpoint !== this.opts.tokenEndpoint || entry.scope !== this.opts.scope || Date.now() >= entry.expiresAt) {
1002
+ await this.opts.tokenCache.clear().catch(() => void 0);
1003
+ return null;
1004
+ }
1005
+ this.token = entry.accessToken;
1006
+ this.expiresAt = entry.expiresAt;
1007
+ return this.token;
1008
+ }
1009
+ async writeCachedToken() {
1010
+ if (!this.opts.tokenCache || !this.token) return;
1011
+ await this.opts.tokenCache.write({
1012
+ accessToken: this.token,
1013
+ expiresAt: this.expiresAt,
1014
+ clientId: this.opts.clientId,
1015
+ tokenEndpoint: this.opts.tokenEndpoint,
1016
+ scope: this.opts.scope
1017
+ });
1018
+ }
1019
+ };
1020
+
1021
+ // src/cli/auth/access-token.ts
1022
+ async function resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io) {
1023
+ const cached = await readFreshCachedToken(tokenCache);
1024
+ if (cached) return cached;
1025
+ const clientId = firstNonEmpty(env.DOCMANA_CLIENT_ID, cliConfig.clientId);
1026
+ const clientSecret = firstNonEmpty(env.DOCMANA_CLIENT_SECRET, cliConfig.clientSecret);
1027
+ if (!clientId || !clientSecret) {
1028
+ throw new CliUsageError(
1029
+ "Missing access token. Use --access-token, DOCMANA_ACCESS_TOKEN, docmana login, or configure OAuth credentials."
1030
+ );
1031
+ }
1032
+ const tokenManager = new TokenManager({
1033
+ clientId,
1034
+ clientSecret,
1035
+ tokenEndpoint: firstNonEmpty(env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint,
1036
+ scope: firstNonEmpty(env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope,
1037
+ fetchImpl: deps.fetch ?? fetch,
1038
+ tokenCache
1039
+ });
1040
+ const token = await tokenManager.getToken();
1041
+ io.stderr("Generated new access token and updated the token cache.\n");
1042
+ return token;
962
1043
  }
963
- function formatResultsPreview(results) {
964
- if (!results) return void 0;
965
- const preview = JSON.stringify(results, null, 2);
966
- if (!preview) return void 0;
967
- return preview.length > 2e3 ? `${preview.slice(0, 2e3)}
968
- ...` : preview;
1044
+ async function readFreshCachedToken(tokenCache) {
1045
+ const cached = await tokenCache.read().catch(() => null);
1046
+ if (!cached || Date.now() >= cached.expiresAt) return void 0;
1047
+ return cached.accessToken;
969
1048
  }
970
- function formatRuntimeError(err) {
971
- return formatHumanError(err);
1049
+ async function resolveAuthConfig(options, env, deps) {
1050
+ const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
1051
+ const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
1052
+ const clientSecret = firstNonEmpty(
1053
+ options.clientSecret,
1054
+ env.DOCMANA_CLIENT_SECRET,
1055
+ cliConfig.clientSecret
1056
+ );
1057
+ const tokenEndpoint = firstNonEmpty(options.tokenUrl, env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint;
1058
+ const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope;
1059
+ if (!clientId) {
1060
+ throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
1061
+ }
1062
+ if (!clientSecret) {
1063
+ throw new CliUsageError("Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET.");
1064
+ }
1065
+ return { clientId, clientSecret, tokenEndpoint, scope };
972
1066
  }
973
- function formatHumanError(err) {
974
- if (err instanceof DocmanaExecutionError) {
975
- const lines = [`Error: ${err.message}`];
976
- if (err.errors && err.errors.length > 0) {
977
- lines.push("Global errors:");
978
- for (const e of err.errors) {
979
- lines.push(` - ${formatSingleError(e)}`);
1067
+
1068
+ // src/cli/commands/api-client.ts
1069
+ async function resolveApiClient(options, env, io, clientFactory, deps) {
1070
+ const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
1071
+ const apiBaseUrl = firstNonEmpty(options.apiUrl, env.DOCMANA_API_URL, cliConfig.apiBaseUrl);
1072
+ if (!apiBaseUrl) {
1073
+ throw new CliUsageError("Missing API base URL. Use --api-url or DOCMANA_API_URL.");
1074
+ }
1075
+ const tokenCache = createFileTokenCache(resolveTokenCachePath(options.tokenCache, getCwd(deps)));
1076
+ const accessToken = firstNonEmpty(options.accessToken, env.DOCMANA_ACCESS_TOKEN) ?? await resolveFlowAccessToken(env, cliConfig, tokenCache, deps, io);
1077
+ return clientFactory({ apiBaseUrl, accessToken });
1078
+ }
1079
+
1080
+ // src/cli/commands/flow-definition.ts
1081
+ async function flowDefinitionCommand(flowId, options, env, io, clientFactory, deps) {
1082
+ try {
1083
+ const client = await resolveApiClient(options, env, io, clientFactory, deps);
1084
+ const definition = await client.getFlowDefinition(flowId);
1085
+ io.stdout(`${JSON.stringify(definition, null, 2)}
1086
+ `);
1087
+ } catch (err) {
1088
+ const errorPayload = {
1089
+ error: err instanceof Error ? err.message : String(err)
1090
+ };
1091
+ if (err instanceof DocmanaError) {
1092
+ errorPayload.code = err.code;
1093
+ if (err.status !== void 0) errorPayload.status = err.status;
1094
+ if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
1095
+ }
1096
+ io.stdout(`${JSON.stringify(errorPayload, null, 2)}
1097
+ `);
1098
+ throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
1099
+ }
1100
+ }
1101
+
1102
+ // src/cli/commands/flow-list.ts
1103
+ async function listFlowsCommand(options, env, io, clientFactory, deps) {
1104
+ try {
1105
+ const client = await resolveApiClient(options, env, io, clientFactory, deps);
1106
+ const flows = await client.listFlows();
1107
+ if (options.json) {
1108
+ io.stdout(`${JSON.stringify(flows, null, 2)}
1109
+ `);
1110
+ return;
1111
+ }
1112
+ io.stdout(formatFlowList(flows));
1113
+ } catch (err) {
1114
+ if (options.json) {
1115
+ const errorPayload = {
1116
+ error: err instanceof Error ? err.message : String(err)
1117
+ };
1118
+ if (err instanceof DocmanaError) {
1119
+ errorPayload.code = err.code;
1120
+ if (err.status !== void 0) errorPayload.status = err.status;
1121
+ if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
980
1122
  }
1123
+ io.stdout(`${JSON.stringify(errorPayload, null, 2)}
1124
+ `);
1125
+ throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
981
1126
  }
982
- const result = err.result;
983
- if (result) {
984
- if (result.documents && result.documents.mappings && result.documents.mappings.length > 0) {
985
- lines.push("Document execution results:");
986
- for (const mapping of result.documents.mappings) {
987
- const docKey = mapping.reference;
988
- const docName = mapping.name;
989
- const docResult = result.documents[docKey];
990
- if (docResult) {
991
- const status = docResult.status || "Unknown";
992
- const nodes = [];
993
- if (docResult.classifications) nodes.push(...Object.values(docResult.classifications));
994
- if (docResult.metadataExtraction) nodes.push(docResult.metadataExtraction);
995
- if (docResult.extractions) nodes.push(...Object.values(docResult.extractions));
996
- if (docResult.validations) nodes.push(...Object.values(docResult.validations));
997
- const hasNodeFailures = nodes.some(
998
- (n) => n.status === "Failed" || n.errors && n.errors.length > 0
999
- );
1000
- if (docResult.status === "Failed" || hasNodeFailures) {
1001
- lines.push(` - Document: ${docName} (Status: ${status})`);
1002
- if (docResult.classifications) {
1003
- for (const [nodeName, node] of Object.entries(docResult.classifications)) {
1004
- if (node.errors && node.errors.length > 0) {
1005
- lines.push(` Classification: ${nodeName} (Status: ${node.status})`);
1006
- for (const nodeErr of node.errors) {
1007
- lines.push(` - ${nodeErr}`);
1008
- }
1009
- }
1010
- }
1011
- }
1012
- if (docResult.metadataExtraction?.errors && docResult.metadataExtraction.errors.length > 0) {
1013
- lines.push(
1014
- ` MetadataExtraction: (Status: ${docResult.metadataExtraction.status})`
1015
- );
1016
- for (const nodeErr of docResult.metadataExtraction.errors) {
1017
- lines.push(` - ${nodeErr}`);
1018
- }
1019
- }
1020
- if (docResult.extractions) {
1021
- for (const [nodeName, node] of Object.entries(docResult.extractions)) {
1022
- if (node.status === "Failed" || node.errors && node.errors.length > 0) {
1023
- lines.push(` Extraction: ${nodeName} (Status: ${node.status})`);
1024
- if (node.errors) {
1025
- for (const nodeErr of node.errors) {
1026
- lines.push(` - ${nodeErr}`);
1027
- }
1028
- }
1029
- }
1030
- }
1031
- }
1032
- if (docResult.validations) {
1033
- for (const [nodeName, node] of Object.entries(docResult.validations)) {
1034
- if (node.status === "Failed" || node.errors && node.errors.length > 0) {
1035
- lines.push(` Validation: ${nodeName} (Status: ${node.status})`);
1036
- if (node.errors) {
1037
- for (const nodeErr of node.errors) {
1038
- lines.push(` - ${nodeErr}`);
1039
- }
1040
- }
1041
- }
1042
- }
1043
- }
1044
- } else {
1045
- lines.push(` - Document: ${docName} (Status: ${status})`);
1046
- }
1047
- } else {
1048
- lines.push(` - Document: ${docName} (Status: Unknown)`);
1049
- }
1050
- }
1127
+ throw err;
1128
+ }
1129
+ }
1130
+
1131
+ // src/cli/commands/flow-metadata.ts
1132
+ async function flowMetadataCommand(flowId, options, env, io, clientFactory, deps) {
1133
+ try {
1134
+ const client = await resolveApiClient(options, env, io, clientFactory, deps);
1135
+ const metadata = await client.getFlowMetadata(flowId);
1136
+ io.stdout(`${JSON.stringify(metadata, null, 2)}
1137
+ `);
1138
+ } catch (err) {
1139
+ const errorPayload = {
1140
+ error: err instanceof Error ? err.message : String(err)
1141
+ };
1142
+ if (err instanceof DocmanaError) {
1143
+ errorPayload.code = err.code;
1144
+ if (err.status !== void 0) errorPayload.status = err.status;
1145
+ if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
1146
+ }
1147
+ io.stdout(`${JSON.stringify(errorPayload, null, 2)}
1148
+ `);
1149
+ throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
1150
+ }
1151
+ }
1152
+
1153
+ // src/cli/commands/organization-document-download.ts
1154
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1155
+ import { dirname as dirname2, join } from "path";
1156
+ async function organizationDocumentDownloadCommand(path, options, env, io, clientFactory, deps) {
1157
+ try {
1158
+ const client = await resolveApiClient(options, env, io, clientFactory, deps);
1159
+ const document = await client.downloadOrganizationDocument(path);
1160
+ const outputPath = options.output ?? join(getCwd(deps), path);
1161
+ await mkdir2(dirname2(outputPath), { recursive: true });
1162
+ await writeFile2(outputPath, document.bytes);
1163
+ io.stdout(`Downloaded ${document.filename} to ${outputPath}
1164
+ `);
1165
+ } catch (err) {
1166
+ const errorPayload = {
1167
+ error: err instanceof Error ? err.message : String(err)
1168
+ };
1169
+ if (err instanceof DocmanaError) {
1170
+ errorPayload.code = err.code;
1171
+ if (err.status !== void 0) errorPayload.status = err.status;
1172
+ if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
1173
+ }
1174
+ io.stdout(`${JSON.stringify(errorPayload, null, 2)}
1175
+ `);
1176
+ throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
1177
+ }
1178
+ }
1179
+
1180
+ // src/cli/commands/organization-document-list.ts
1181
+ async function organizationDocumentListCommand(options, env, io, clientFactory, deps) {
1182
+ try {
1183
+ const client = await resolveApiClient(options, env, io, clientFactory, deps);
1184
+ const payload = await client.listOrganizationDocuments({ prefix: options.prefix });
1185
+ if (options.json) {
1186
+ io.stdout(`${JSON.stringify(payload, null, 2)}
1187
+ `);
1188
+ return;
1189
+ }
1190
+ if (!payload.configured) {
1191
+ io.stdout("Organization document depot is not configured.\n");
1192
+ return;
1193
+ }
1194
+ io.stdout(formatOrganizationDocumentList(payload.documents));
1195
+ } catch (err) {
1196
+ const errorPayload = {
1197
+ error: err instanceof Error ? err.message : String(err)
1198
+ };
1199
+ if (err instanceof DocmanaError) {
1200
+ errorPayload.code = err.code;
1201
+ if (err.status !== void 0) errorPayload.status = err.status;
1202
+ if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
1203
+ }
1204
+ io.stdout(`${JSON.stringify(errorPayload, null, 2)}
1205
+ `);
1206
+ throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
1207
+ }
1208
+ }
1209
+
1210
+ // src/cli/commands/organization-list.ts
1211
+ async function listOrganizationsCommand(options, env, io, clientFactory, deps) {
1212
+ try {
1213
+ const client = await resolveApiClient(options, env, io, clientFactory, deps);
1214
+ const organizations = await client.listOrganizations();
1215
+ if (options.json) {
1216
+ io.stdout(`${JSON.stringify(organizations, null, 2)}
1217
+ `);
1218
+ return;
1219
+ }
1220
+ io.stdout(formatOrganizationList(organizations));
1221
+ } catch (err) {
1222
+ if (options.json) {
1223
+ const errorPayload = {
1224
+ error: err instanceof Error ? err.message : String(err)
1225
+ };
1226
+ if (err instanceof DocmanaError) {
1227
+ errorPayload.code = err.code;
1228
+ if (err.status !== void 0) errorPayload.status = err.status;
1229
+ if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
1051
1230
  }
1231
+ io.stdout(`${JSON.stringify(errorPayload, null, 2)}
1232
+ `);
1233
+ throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
1052
1234
  }
1053
- return lines.join("\n");
1235
+ throw err;
1054
1236
  }
1055
- if (err instanceof DocmanaError) {
1056
- return `Error: ${err.message} (Code: ${err.code})`;
1237
+ }
1238
+
1239
+ // src/cli/commands/flow-run.ts
1240
+ async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
1241
+ try {
1242
+ const files = parseFiles(options.files);
1243
+ const client = await resolveApiClient(options, env, io, clientFactory, deps);
1244
+ const result = await client.runFlow(
1245
+ flowId,
1246
+ {
1247
+ files: files.map((path) => ({ path })),
1248
+ language: options.language,
1249
+ useDraftVersion: options.draft === true
1250
+ },
1251
+ {
1252
+ onPoll: (event) => {
1253
+ const nextPoll = event.nextPollAfterMs === void 0 ? "" : `, next poll in ${formatElapsedSeconds(event.nextPollAfterMs)}`;
1254
+ io.stderr(
1255
+ `Polling execution ${event.executionResultId}: ${event.status} (attempt ${event.attempt}, elapsed ${formatElapsedSeconds(event.elapsedMs)}${nextPoll})
1256
+ `
1257
+ );
1258
+ },
1259
+ onRateLimit: (event) => {
1260
+ io.stderr(
1261
+ `Polling execution ${event.executionResultId}: rate limited (attempt ${event.attempt}, retrying in ${formatElapsedSeconds(event.retryAfterMs)})
1262
+ `
1263
+ );
1264
+ }
1265
+ }
1266
+ );
1267
+ if (options.json) {
1268
+ io.stdout(`${JSON.stringify(result, null, 2)}
1269
+ `);
1270
+ return;
1271
+ }
1272
+ io.stdout(formatHumanResult(result));
1273
+ } catch (err) {
1274
+ if (options.json) {
1275
+ if (err instanceof DocmanaExecutionError && err.result) {
1276
+ io.stdout(`${JSON.stringify(err.result, null, 2)}
1277
+ `);
1278
+ } else {
1279
+ const errorPayload = {
1280
+ error: err instanceof Error ? err.message : String(err)
1281
+ };
1282
+ if (err instanceof DocmanaError) {
1283
+ errorPayload.code = err.code;
1284
+ if (err.status !== void 0) errorPayload.status = err.status;
1285
+ if (err.requestId !== void 0) errorPayload.requestId = err.requestId;
1286
+ }
1287
+ io.stdout(`${JSON.stringify(errorPayload, null, 2)}
1288
+ `);
1289
+ }
1290
+ throw new CliSilentError(err instanceof CliUsageError ? 2 : 1);
1291
+ }
1292
+ throw err;
1057
1293
  }
1058
- if (err instanceof Error) {
1059
- return `Error: ${err.message}`;
1294
+ }
1295
+
1296
+ // src/cli/commands/login.ts
1297
+ async function loginCommand(options, env, io, deps) {
1298
+ const auth = await resolveAuthConfig(options, env, deps);
1299
+ const tokenCachePath = resolveTokenCachePath(options.tokenCache, getCwd(deps));
1300
+ const tokenCache = createFileTokenCache(tokenCachePath);
1301
+ const hadFreshCachedToken = Boolean(await readFreshCachedToken(tokenCache));
1302
+ const tokenManager = new TokenManager({
1303
+ clientId: auth.clientId,
1304
+ clientSecret: auth.clientSecret,
1305
+ tokenEndpoint: auth.tokenEndpoint,
1306
+ scope: auth.scope,
1307
+ fetchImpl: deps.fetch ?? fetch,
1308
+ tokenCache
1309
+ });
1310
+ await tokenManager.getToken();
1311
+ const cached = await tokenCache.read();
1312
+ if (!hadFreshCachedToken) {
1313
+ io.stdout("Generated new access token and updated the token cache.\n");
1060
1314
  }
1061
- return `Error: ${String(err)}`;
1315
+ io.stdout(`Logged in. Token cached at ${tokenCachePath}
1316
+ `);
1317
+ if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
1062
1318
  }
1063
- function formatSingleError(e) {
1064
- if (typeof e === "string") return e;
1065
- if (e && typeof e === "object") {
1066
- const obj = e;
1067
- if (typeof obj.msg === "string") return obj.msg;
1068
- if (typeof obj.message === "string") return obj.message;
1069
- if (typeof obj.error === "string") return obj.error;
1070
- try {
1071
- return JSON.stringify(e);
1072
- } catch {
1073
- return String(e);
1319
+ function formatTokenExpiry(expiresAt) {
1320
+ const expiresAtDate = new Date(expiresAt);
1321
+ return [
1322
+ `Token expires at local time ${expiresAtDate.toLocaleString()}`,
1323
+ `Token expires at UTC ${expiresAtDate.toISOString()}`
1324
+ ].join("\n") + "\n";
1325
+ }
1326
+
1327
+ // src/cli/cli.ts
1328
+ async function runCli(argv = process.argv.slice(2), env = process.env, io = {
1329
+ stdout: (text) => process.stdout.write(text),
1330
+ stderr: (text) => process.stderr.write(text)
1331
+ }, clientFactory = (config) => new Docmana(config), deps = {}) {
1332
+ const program = buildProgram(env, io, clientFactory, deps);
1333
+ if (argv.length === 0) {
1334
+ io.stdout(program.helpInformation());
1335
+ return 0;
1336
+ }
1337
+ try {
1338
+ await program.parseAsync(argv, { from: "user" });
1339
+ return 0;
1340
+ } catch (err) {
1341
+ if (err instanceof CliSilentError) {
1342
+ return err.exitCode;
1343
+ }
1344
+ if (err instanceof CliUsageError) {
1345
+ io.stderr(`Error: ${err.message}
1346
+ `);
1347
+ return 2;
1348
+ }
1349
+ if (err instanceof CommanderError) {
1350
+ return err.exitCode === 0 ? 0 : 2;
1074
1351
  }
1352
+ io.stderr(`${formatRuntimeError(err)}
1353
+ `);
1354
+ return 1;
1075
1355
  }
1076
- return String(e);
1356
+ }
1357
+ function buildProgram(env, io, clientFactory, deps) {
1358
+ const program = new Command();
1359
+ program.name("docmana").description(HELP.root).showHelpAfterError().configureOutput({
1360
+ writeOut: io.stdout,
1361
+ writeErr: io.stderr,
1362
+ outputError: (str, write) => write(str)
1363
+ }).exitOverride();
1364
+ program.command("login").description(HELP.login).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("--token-url <url>", "OAuth2 token endpoint; defaults to DOCMANA_TOKEN_URL").option("--scope <scope>", "OAuth2 scope; defaults to DOCMANA_SCOPE").option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).option("--token-cache <path>", `Token cache path; defaults to ./${DEFAULT_TOKEN_CACHE_FILE}`).action(async (options) => {
1365
+ await loginCommand(options, env, io, deps);
1366
+ });
1367
+ const flow = program.command("flow").description(HELP.flow).action(() => {
1368
+ io.stdout(flow.helpInformation());
1369
+ });
1370
+ flow.command("list").description(HELP.list).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 flow list").action(async (options) => {
1371
+ await listFlowsCommand(options, env, io, clientFactory, deps);
1372
+ });
1373
+ 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) => {
1374
+ await runFlowCommand(flowId, options, env, io, clientFactory, deps);
1375
+ });
1376
+ flow.command("metadata").description(HELP.metadata).argument("<flow-id>", "Docmana flow id").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}`).action(async (flowId, options) => {
1377
+ await flowMetadataCommand(flowId, options, env, io, clientFactory, deps);
1378
+ });
1379
+ flow.command("definition").description(HELP.definition).argument("<flow-id>", "Docmana flow id").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}`).action(async (flowId, options) => {
1380
+ await flowDefinitionCommand(flowId, options, env, io, clientFactory, deps);
1381
+ });
1382
+ const organization = program.command("organization").description(HELP.organization).action(() => {
1383
+ io.stdout(organization.helpInformation());
1384
+ });
1385
+ organization.command("list").description(HELP.organizationList).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 organization list").action(async (options) => {
1386
+ await listOrganizationsCommand(options, env, io, clientFactory, deps);
1387
+ });
1388
+ const organizationDocument = organization.command("document").description(HELP.organizationDocument).action(() => {
1389
+ io.stdout(organizationDocument.helpInformation());
1390
+ });
1391
+ organizationDocument.command("list").description(HELP.organizationDocumentList).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("--prefix <prefix>", "Filter documents by path prefix").option("--json", "Print the raw JSON organization document list").action(async (options) => {
1392
+ await organizationDocumentListCommand(options, env, io, clientFactory, deps);
1393
+ });
1394
+ organizationDocument.command("download").description(HELP.organizationDocumentDownload).argument("<path>", "Organization document path").option("--output <file>", "Output file path; defaults to ./{path}").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}`).action(async (path, options) => {
1395
+ await organizationDocumentDownloadCommand(path, options, env, io, clientFactory, deps);
1396
+ });
1397
+ const config = program.command("config").description(HELP.config).action(() => {
1398
+ io.stdout(config.helpInformation());
1399
+ });
1400
+ config.command("init").description(HELP.configInit).option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).action(async (options) => {
1401
+ await configInitCommand(options, io, deps);
1402
+ });
1403
+ return program;
1077
1404
  }
1078
1405
  function isDirectRun() {
1079
1406
  const entry = process.argv[1];
1080
1407
  if (!entry) return false;
1081
1408
  try {
1082
- return realpathSync(resolve(entry)) === realpathSync(fileURLToPath(import.meta.url));
1409
+ return realpathSync(resolve2(entry)) === realpathSync(fileURLToPath(import.meta.url));
1083
1410
  } catch {
1084
1411
  return false;
1085
1412
  }