@docmana/sdk 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs ADDED
@@ -0,0 +1,772 @@
1
+ #!/usr/bin/env node
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";
8
+ import { fileURLToPath } from "url";
9
+ import { Command, CommanderError } from "commander";
10
+
11
+ // src/errors.ts
12
+ var DocmanaError = class extends Error {
13
+ status;
14
+ requestId;
15
+ code;
16
+ constructor(message, opts = {}) {
17
+ super(message);
18
+ this.name = new.target.name;
19
+ this.status = opts.status;
20
+ this.requestId = opts.requestId;
21
+ this.code = opts.code ?? "docmana_error";
22
+ Object.setPrototypeOf(this, new.target.prototype);
23
+ }
24
+ };
25
+ var DocmanaAuthError = class extends DocmanaError {
26
+ constructor(message, opts = {}) {
27
+ super(message, { ...opts, code: "auth_error" });
28
+ }
29
+ };
30
+ var DocmanaPermissionError = class extends DocmanaError {
31
+ constructor(message, opts = {}) {
32
+ super(message, { ...opts, code: "permission_error" });
33
+ }
34
+ };
35
+ var DocmanaNotFoundError = class extends DocmanaError {
36
+ constructor(message, opts = {}) {
37
+ super(message, { ...opts, code: "not_found" });
38
+ }
39
+ };
40
+ var DocmanaRequestError = class extends DocmanaError {
41
+ constructor(message, opts = {}) {
42
+ super(message, { ...opts, code: "bad_request" });
43
+ }
44
+ };
45
+ var DocmanaExecutionError = class extends DocmanaError {
46
+ errors;
47
+ constructor(message, errors = []) {
48
+ super(message, { code: "execution_failed" });
49
+ this.errors = errors;
50
+ }
51
+ };
52
+ var DocmanaTimeoutError = class extends DocmanaError {
53
+ constructor(message) {
54
+ super(message, { code: "timeout" });
55
+ }
56
+ };
57
+ var DocmanaAbortError = class extends DocmanaError {
58
+ constructor(message) {
59
+ super(message, { code: "aborted" });
60
+ }
61
+ };
62
+ function errorFromResponse(status, message, requestId) {
63
+ const opts = { status, requestId };
64
+ switch (status) {
65
+ case 400:
66
+ return new DocmanaRequestError(message, opts);
67
+ case 401:
68
+ return new DocmanaAuthError(message, opts);
69
+ case 403:
70
+ return new DocmanaPermissionError(message, opts);
71
+ case 404:
72
+ return new DocmanaNotFoundError(message, opts);
73
+ default:
74
+ return new DocmanaError(message, opts);
75
+ }
76
+ }
77
+
78
+ // src/auth/token-manager.ts
79
+ var SAFETY_MARGIN_MS = 6e4;
80
+ var TokenManager = class {
81
+ constructor(opts) {
82
+ this.opts = opts;
83
+ }
84
+ opts;
85
+ token = null;
86
+ expiresAt = 0;
87
+ inflight = null;
88
+ invalidate() {
89
+ this.token = null;
90
+ this.expiresAt = 0;
91
+ void this.opts.tokenCache?.clear().catch(() => void 0);
92
+ }
93
+ async getToken() {
94
+ if (this.token && Date.now() < this.expiresAt) return this.token;
95
+ const cached = await this.readCachedToken();
96
+ if (cached) return cached;
97
+ if (this.inflight) return this.inflight;
98
+ this.inflight = this.acquire().finally(() => {
99
+ this.inflight = null;
100
+ });
101
+ return this.inflight;
102
+ }
103
+ async acquire() {
104
+ const body = new URLSearchParams({
105
+ grant_type: "client_credentials",
106
+ client_id: this.opts.clientId,
107
+ client_secret: this.opts.clientSecret,
108
+ scope: this.opts.scope
109
+ });
110
+ const res = await this.opts.fetchImpl(this.opts.tokenEndpoint, {
111
+ method: "POST",
112
+ headers: { "content-type": "application/x-www-form-urlencoded" },
113
+ body: body.toString()
114
+ });
115
+ if (!res.ok) {
116
+ throw new DocmanaAuthError("Failed to acquire Docmana access token", { status: res.status });
117
+ }
118
+ const json = await res.json();
119
+ if (typeof json.access_token !== "string" || json.access_token.length === 0 || typeof json.expires_in !== "number" || !Number.isFinite(json.expires_in)) {
120
+ throw new DocmanaAuthError("Malformed token response from Docmana CIAM", {
121
+ status: res.status
122
+ });
123
+ }
124
+ this.token = json.access_token;
125
+ this.expiresAt = Date.now() + json.expires_in * 1e3 - SAFETY_MARGIN_MS;
126
+ await this.writeCachedToken().catch(() => void 0);
127
+ return this.token;
128
+ }
129
+ async readCachedToken() {
130
+ if (!this.opts.tokenCache) return null;
131
+ let entry;
132
+ try {
133
+ entry = await this.opts.tokenCache.read();
134
+ } catch {
135
+ return null;
136
+ }
137
+ if (!entry) return null;
138
+ if (entry.clientId !== this.opts.clientId || entry.tokenEndpoint !== this.opts.tokenEndpoint || entry.scope !== this.opts.scope || Date.now() >= entry.expiresAt) {
139
+ await this.opts.tokenCache.clear().catch(() => void 0);
140
+ return null;
141
+ }
142
+ this.token = entry.accessToken;
143
+ this.expiresAt = entry.expiresAt;
144
+ return this.token;
145
+ }
146
+ async writeCachedToken() {
147
+ if (!this.opts.tokenCache || !this.token) return;
148
+ await this.opts.tokenCache.write({
149
+ accessToken: this.token,
150
+ expiresAt: this.expiresAt,
151
+ clientId: this.opts.clientId,
152
+ tokenEndpoint: this.opts.tokenEndpoint,
153
+ scope: this.opts.scope
154
+ });
155
+ }
156
+ };
157
+
158
+ // src/config.ts
159
+ var DEFAULTS = {
160
+ apiBaseUrl: "https://api.docmana.ai",
161
+ tokenEndpoint: "https://4fe70f5b-e013-4f65-9fa7-3109a33beba5.ciamlogin.com/4fe70f5b-e013-4f65-9fa7-3109a33beba5/oauth2/v2.0/token",
162
+ scope: "api://d781e6ba-cc08-4618-8099-ad968abd2b9e/.default",
163
+ timeoutMs: 3e5,
164
+ pollIntervalMs: 2e3
165
+ };
166
+ function resolveConfig(config) {
167
+ if (!config.clientId) throw new Error("DocmanaConfig.clientId is required");
168
+ if (!config.clientSecret) throw new Error("DocmanaConfig.clientSecret is required");
169
+ const apiBaseUrl = (config.apiBaseUrl ?? DEFAULTS.apiBaseUrl).replace(/\/+$/, "");
170
+ return {
171
+ clientId: config.clientId,
172
+ clientSecret: config.clientSecret,
173
+ apiBaseUrl,
174
+ tokenEndpoint: config.tokenEndpoint ?? DEFAULTS.tokenEndpoint,
175
+ scope: config.scope ?? DEFAULTS.scope,
176
+ timeoutMs: config.timeoutMs ?? DEFAULTS.timeoutMs,
177
+ pollIntervalMs: config.pollIntervalMs ?? DEFAULTS.pollIntervalMs,
178
+ fetchImpl: config.fetch ?? fetch,
179
+ headers: config.headers ?? {},
180
+ tokenCache: config.tokenCache
181
+ };
182
+ }
183
+
184
+ // src/http/http-client.ts
185
+ var HttpClient = class {
186
+ constructor(opts) {
187
+ this.opts = opts;
188
+ }
189
+ opts;
190
+ async requestJson(method, path, options = {}) {
191
+ const res = await this.requestRaw(method, path, options);
192
+ const text = await res.text();
193
+ if (!text) return {};
194
+ try {
195
+ return JSON.parse(text);
196
+ } catch {
197
+ throw new DocmanaError("Invalid JSON response from Docmana", { status: res.status });
198
+ }
199
+ }
200
+ async requestRaw(method, path, options = {}) {
201
+ let res = await this.send(method, path, options);
202
+ if (res.status === 401) {
203
+ this.opts.tokenManager.invalidate();
204
+ res = await this.send(method, path, options);
205
+ }
206
+ if (!res.ok) {
207
+ const requestId = res.headers.get("x-request-id") ?? void 0;
208
+ const message = await this.extractMessage(res);
209
+ throw errorFromResponse(res.status, message, requestId);
210
+ }
211
+ return res;
212
+ }
213
+ async send(method, path, options) {
214
+ const token = await this.opts.tokenManager.getToken();
215
+ const url = new URL(this.opts.apiBaseUrl + path);
216
+ for (const [k, v] of Object.entries(options.query ?? {})) url.searchParams.set(k, v);
217
+ const headers = {
218
+ ...this.opts.headers ?? {},
219
+ authorization: `Bearer ${token}`,
220
+ ...options.headers ?? {}
221
+ };
222
+ try {
223
+ return await this.opts.fetchImpl(url, {
224
+ method,
225
+ headers,
226
+ body: options.body,
227
+ signal: options.signal
228
+ });
229
+ } catch (err) {
230
+ if (options.signal?.aborted || err instanceof Error && err.name === "AbortError") {
231
+ throw new DocmanaAbortError("Request aborted");
232
+ }
233
+ throw err;
234
+ }
235
+ }
236
+ async extractMessage(res) {
237
+ try {
238
+ const data = await res.clone().json();
239
+ return data.message ?? data.error ?? res.statusText ?? `HTTP ${res.status}`;
240
+ } catch {
241
+ return res.statusText ?? `HTTP ${res.status}`;
242
+ }
243
+ }
244
+ };
245
+
246
+ // src/files/resolve-input.ts
247
+ import { readFile } from "fs/promises";
248
+ import { basename } from "path";
249
+ var CONTENT_TYPES = {
250
+ ".pdf": "application/pdf",
251
+ ".png": "image/png",
252
+ ".jpg": "image/jpeg",
253
+ ".jpeg": "image/jpeg",
254
+ ".tif": "image/tiff",
255
+ ".tiff": "image/tiff",
256
+ ".txt": "text/plain",
257
+ ".json": "application/json"
258
+ };
259
+ function contentTypeFor(name) {
260
+ const dot = name.lastIndexOf(".");
261
+ if (dot < 0) return "application/octet-stream";
262
+ const ext = name.slice(dot).toLowerCase();
263
+ return CONTENT_TYPES[ext] ?? "application/octet-stream";
264
+ }
265
+ function isReadable(x) {
266
+ return typeof x === "object" && x !== null && typeof x.pipe === "function";
267
+ }
268
+ function isPathInput(x) {
269
+ return typeof x === "object" && x !== null && typeof x.path === "string";
270
+ }
271
+ async function streamToBuffer(stream) {
272
+ const chunks = [];
273
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
274
+ return Buffer.concat(chunks);
275
+ }
276
+ async function resolveOne(input, fallbackName) {
277
+ if (typeof input === "string" || isPathInput(input)) {
278
+ const path = typeof input === "string" ? input : input.path;
279
+ const data = await readFile(path);
280
+ const filename = basename(path);
281
+ return { data: new Blob([data]), filename, contentType: contentTypeFor(filename) };
282
+ }
283
+ if (input instanceof Uint8Array) {
284
+ return {
285
+ data: new Blob([input]),
286
+ filename: fallbackName,
287
+ contentType: contentTypeFor(fallbackName)
288
+ };
289
+ }
290
+ if (isReadable(input)) {
291
+ const buf = await streamToBuffer(input);
292
+ return {
293
+ data: new Blob([buf]),
294
+ filename: fallbackName,
295
+ contentType: contentTypeFor(fallbackName)
296
+ };
297
+ }
298
+ throw new Error("Unsupported file input type");
299
+ }
300
+ async function resolveInputs(input) {
301
+ const list = input.files ?? (input.file !== void 0 ? [input.file] : []);
302
+ if (list.length === 0) throw new Error("runFlow requires `file` or `files`");
303
+ const fallback = input.fileName ?? "upload.bin";
304
+ return Promise.all(list.map((item) => resolveOne(item, fallback)));
305
+ }
306
+
307
+ // src/api/upload.ts
308
+ async function uploadFiles(http, parts, signal) {
309
+ const ids = [];
310
+ for (const part of parts) {
311
+ const form = new FormData();
312
+ form.append("files", new File([part.data], part.filename, { type: part.contentType }));
313
+ const res = await http.requestJson("POST", "/upload", { body: form, signal });
314
+ ids.push(res.id);
315
+ }
316
+ return ids;
317
+ }
318
+
319
+ // src/api/run-flow.ts
320
+ async function runFlow(http, flowId, uuidFiles, signal, once = false) {
321
+ const path = once ? `/flows/run_once_flow/${flowId}` : `/flows/run_flow/${flowId}`;
322
+ const res = await http.requestJson("POST", path, {
323
+ query: { async_mode: "true" },
324
+ headers: { "content-type": "application/json" },
325
+ body: JSON.stringify({ uuid_files: uuidFiles }),
326
+ signal
327
+ });
328
+ return res.execution_result_id;
329
+ }
330
+
331
+ // src/api/execution-status.ts
332
+ async function getStatus(http, executionResultId, signal) {
333
+ return http.requestJson("GET", `/flows/execution-status/${executionResultId}`, { signal });
334
+ }
335
+
336
+ // src/api/execution-result.ts
337
+ async function getResult(http, executionResultId, signal) {
338
+ return http.requestJson("GET", `/flows/execution-result/${executionResultId}`, { signal });
339
+ }
340
+
341
+ // src/polling/poll.ts
342
+ var TERMINAL = /* @__PURE__ */ new Set(["Completed", "Failed"]);
343
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
344
+ async function pollUntilTerminal(opts) {
345
+ const sleep = opts.sleep ?? defaultSleep;
346
+ const start = Date.now();
347
+ const elapsed = opts.elapsed ?? (() => Date.now() - start);
348
+ for (; ; ) {
349
+ if (opts.signal?.aborted) throw new DocmanaAbortError("Polling aborted");
350
+ const status = await opts.check();
351
+ if (TERMINAL.has(status)) return status;
352
+ if (elapsed() >= opts.timeoutMs) {
353
+ throw new DocmanaTimeoutError(`Execution did not finish within ${opts.timeoutMs}ms`);
354
+ }
355
+ await sleep(opts.intervalMs);
356
+ }
357
+ }
358
+
359
+ // src/client.ts
360
+ var Docmana = class {
361
+ config;
362
+ http;
363
+ constructor(config) {
364
+ this.config = resolveConfig(config);
365
+ const tokenManager = new TokenManager({
366
+ clientId: this.config.clientId,
367
+ clientSecret: this.config.clientSecret,
368
+ tokenEndpoint: this.config.tokenEndpoint,
369
+ scope: this.config.scope,
370
+ fetchImpl: this.config.fetchImpl,
371
+ tokenCache: this.config.tokenCache
372
+ });
373
+ this.http = new HttpClient({
374
+ apiBaseUrl: this.config.apiBaseUrl,
375
+ fetchImpl: this.config.fetchImpl,
376
+ tokenManager,
377
+ headers: this.config.headers
378
+ });
379
+ }
380
+ async runFlowAsync(flowId, input) {
381
+ const parts = await resolveInputs(input);
382
+ const fileIds = await uploadFiles(this.http, parts, input.signal);
383
+ const executionResultId = await runFlow(
384
+ this.http,
385
+ flowId,
386
+ fileIds,
387
+ input.signal,
388
+ input.once ?? false
389
+ );
390
+ return { executionResultId, fileIds };
391
+ }
392
+ async getStatus(executionResultId, signal) {
393
+ return getStatus(this.http, executionResultId, signal);
394
+ }
395
+ async getResult(executionResultId, signal) {
396
+ return getResult(this.http, executionResultId, signal);
397
+ }
398
+ async runFlow(flowId, input) {
399
+ const { executionResultId } = await this.runFlowAsync(flowId, input);
400
+ await pollUntilTerminal({
401
+ check: async () => (await this.getStatus(executionResultId, input.signal)).status,
402
+ intervalMs: input.pollIntervalMs ?? this.config.pollIntervalMs,
403
+ timeoutMs: input.timeoutMs ?? this.config.timeoutMs,
404
+ signal: input.signal
405
+ });
406
+ const result = await this.getResult(executionResultId, input.signal);
407
+ if (result.status === "Failed") {
408
+ throw new DocmanaExecutionError(`Flow ${flowId} execution failed`, result.errors ?? []);
409
+ }
410
+ return result;
411
+ }
412
+ };
413
+
414
+ // src/cli.ts
415
+ var CliUsageError = class extends Error {
416
+ constructor(message) {
417
+ super(message);
418
+ this.name = "CliUsageError";
419
+ }
420
+ };
421
+ var HELP = {
422
+ root: "Run Docmana document-analysis flows.",
423
+ flow: "Manage and run Docmana flows.",
424
+ config: "Manage Docmana CLI configuration.",
425
+ configInit: "Create or update a local Docmana CLI config file.",
426
+ login: "Acquire and cache a Docmana OAuth2 access token.",
427
+ run: "Upload documents, run a Docmana flow, wait for completion, and print the result."
428
+ };
429
+ var DEFAULT_CONFIG_FILE = "docmana.config.json";
430
+ var DEFAULT_TOKEN_CACHE_FILE = "docmana.token.json";
431
+ async function runCli(argv = process.argv.slice(2), env = process.env, io = {
432
+ stdout: (text) => process.stdout.write(text),
433
+ stderr: (text) => process.stderr.write(text)
434
+ }, clientFactory = (config) => new Docmana(config), deps = {}) {
435
+ const program = buildProgram(env, io, clientFactory, deps);
436
+ if (argv.length === 0) {
437
+ io.stdout(program.helpInformation());
438
+ return 0;
439
+ }
440
+ try {
441
+ await program.parseAsync(argv, { from: "user" });
442
+ return 0;
443
+ } catch (err) {
444
+ if (err instanceof CliUsageError) {
445
+ io.stderr(`Error: ${err.message}
446
+ `);
447
+ return 2;
448
+ }
449
+ if (err instanceof CommanderError) {
450
+ return err.exitCode === 0 ? 0 : 2;
451
+ }
452
+ io.stderr(`${formatRuntimeError(err)}
453
+ `);
454
+ return 1;
455
+ }
456
+ }
457
+ function buildProgram(env, io, clientFactory, deps) {
458
+ const program = new Command();
459
+ program.name("docmana").description(HELP.root).showHelpAfterError().configureOutput({
460
+ writeOut: io.stdout,
461
+ writeErr: io.stderr,
462
+ outputError: (str, write) => write(str)
463
+ }).exitOverride();
464
+ 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) => {
465
+ await loginCommand(options, env, io, deps);
466
+ });
467
+ const flow = program.command("flow").description(HELP.flow).action(() => {
468
+ io.stdout(flow.helpInformation());
469
+ });
470
+ 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").action(async (flowId, options) => {
471
+ await runFlowCommand(flowId, options, env, io, clientFactory, deps);
472
+ });
473
+ const config = program.command("config").description(HELP.config).action(() => {
474
+ io.stdout(config.helpInformation());
475
+ });
476
+ config.command("init").description(HELP.configInit).option("--config <path>", `Config file path; defaults to ./${DEFAULT_CONFIG_FILE}`).action(async (options) => {
477
+ await configInitCommand(options, io, deps);
478
+ });
479
+ return program;
480
+ }
481
+ async function runFlowCommand(flowId, options, env, io, clientFactory, deps) {
482
+ const files = parseFiles(options.files);
483
+ const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
484
+ const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
485
+ const clientSecret = firstNonEmpty(
486
+ options.clientSecret,
487
+ env.DOCMANA_CLIENT_SECRET,
488
+ cliConfig.clientSecret
489
+ );
490
+ const organisation = firstNonEmpty(
491
+ options.organisation,
492
+ env.DOCMANA_ORGANISATION,
493
+ cliConfig.organisation
494
+ );
495
+ const apiBaseUrl = firstNonEmpty(options.apiUrl, env.DOCMANA_API_URL, cliConfig.apiBaseUrl);
496
+ const tokenEndpoint = firstNonEmpty(
497
+ options.tokenUrl,
498
+ env.DOCMANA_TOKEN_URL,
499
+ cliConfig.tokenEndpoint
500
+ );
501
+ const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope);
502
+ if (!clientId)
503
+ throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
504
+ if (!clientSecret) {
505
+ throw new CliUsageError("Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET.");
506
+ }
507
+ if (!organisation) {
508
+ throw new CliUsageError("Missing organisation. Use --organisation or DOCMANA_ORGANISATION.");
509
+ }
510
+ const client = clientFactory({
511
+ clientId,
512
+ clientSecret,
513
+ apiBaseUrl,
514
+ tokenEndpoint,
515
+ scope,
516
+ headers: { "X-Selected-Organization": organisation },
517
+ tokenCache: createFileTokenCache(resolveTokenCachePath(options.tokenCache, getCwd(deps)))
518
+ });
519
+ const result = await client.runFlow(flowId, { files: files.map((path) => ({ path })) });
520
+ if (options.json) {
521
+ io.stdout(`${JSON.stringify(result, null, 2)}
522
+ `);
523
+ return;
524
+ }
525
+ io.stdout(formatHumanResult(result));
526
+ }
527
+ async function loginCommand(options, env, io, deps) {
528
+ const auth = await resolveAuthConfig(options, env, deps);
529
+ const tokenCachePath = resolveTokenCachePath(options.tokenCache, getCwd(deps));
530
+ const tokenCache = createFileTokenCache(tokenCachePath);
531
+ const tokenManager = new TokenManager({
532
+ clientId: auth.clientId,
533
+ clientSecret: auth.clientSecret,
534
+ tokenEndpoint: auth.tokenEndpoint,
535
+ scope: auth.scope,
536
+ fetchImpl: deps.fetch ?? fetch,
537
+ tokenCache
538
+ });
539
+ await tokenManager.getToken();
540
+ const cached = await tokenCache.read();
541
+ io.stdout(`Logged in. Token cached at ${tokenCachePath}
542
+ `);
543
+ if (cached) io.stdout(formatTokenExpiry(cached.expiresAt));
544
+ }
545
+ function formatTokenExpiry(expiresAt) {
546
+ const expiresAtDate = new Date(expiresAt);
547
+ return [
548
+ `Token expires at local time ${expiresAtDate.toLocaleString()}`,
549
+ `Token expires at UTC ${expiresAtDate.toISOString()}`
550
+ ].join("\n") + "\n";
551
+ }
552
+ async function resolveAuthConfig(options, env, deps) {
553
+ const cliConfig = await loadCliConfig(options.config, getCwd(deps), Boolean(options.config));
554
+ const clientId = firstNonEmpty(options.clientId, env.DOCMANA_CLIENT_ID, cliConfig.clientId);
555
+ const clientSecret = firstNonEmpty(
556
+ options.clientSecret,
557
+ env.DOCMANA_CLIENT_SECRET,
558
+ cliConfig.clientSecret
559
+ );
560
+ const tokenEndpoint = firstNonEmpty(options.tokenUrl, env.DOCMANA_TOKEN_URL, cliConfig.tokenEndpoint) ?? DEFAULTS.tokenEndpoint;
561
+ const scope = firstNonEmpty(options.scope, env.DOCMANA_SCOPE, cliConfig.scope) ?? DEFAULTS.scope;
562
+ if (!clientId)
563
+ throw new CliUsageError("Missing client id. Use --client-id or DOCMANA_CLIENT_ID.");
564
+ if (!clientSecret) {
565
+ throw new CliUsageError("Missing client secret. Use --client-secret or DOCMANA_CLIENT_SECRET.");
566
+ }
567
+ return { clientId, clientSecret, tokenEndpoint, scope };
568
+ }
569
+ async function configInitCommand(options, io, deps) {
570
+ const cwd = getCwd(deps);
571
+ const configPath = resolveConfigPath(options.config, cwd);
572
+ const existing = await loadCliConfig(options.config, cwd, false);
573
+ const defaultPrompt = deps.prompt ? void 0 : createDefaultPrompt();
574
+ const prompt = deps.prompt ?? defaultPrompt?.prompt;
575
+ if (!prompt) throw new CliUsageError("Unable to initialize interactive prompt");
576
+ try {
577
+ const config = {
578
+ apiBaseUrl: await prompt("API base URL", {
579
+ defaultValue: existing.apiBaseUrl ?? DEFAULTS.apiBaseUrl
580
+ }),
581
+ tokenEndpoint: await prompt("OAuth2 token endpoint", {
582
+ defaultValue: existing.tokenEndpoint ?? DEFAULTS.tokenEndpoint
583
+ }),
584
+ scope: await prompt("OAuth2 scope", {
585
+ defaultValue: existing.scope ?? DEFAULTS.scope
586
+ }),
587
+ organisation: await prompt("Organisation", {
588
+ defaultValue: existing.organisation
589
+ }),
590
+ clientId: await prompt("Client id", {
591
+ defaultValue: existing.clientId
592
+ }),
593
+ clientSecret: await prompt("Client secret", {
594
+ defaultValue: existing.clientSecret,
595
+ secret: true
596
+ })
597
+ };
598
+ await mkdir(dirname(configPath), { recursive: true });
599
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
600
+ `, "utf8");
601
+ io.stdout(`Wrote ${configPath}
602
+ `);
603
+ io.stdout(
604
+ `Warning: ${DEFAULT_CONFIG_FILE} contains credentials and should stay ignored by Git.
605
+ `
606
+ );
607
+ } finally {
608
+ defaultPrompt?.close();
609
+ }
610
+ }
611
+ async function loadCliConfig(configPathOption, cwd, requireExisting) {
612
+ const configPath = resolveConfigPath(configPathOption, cwd);
613
+ if (!existsSync(configPath)) {
614
+ if (requireExisting) throw new CliUsageError(`Config file not found: ${configPath}`);
615
+ return {};
616
+ }
617
+ let raw;
618
+ try {
619
+ raw = await readFile2(configPath, "utf8");
620
+ } catch {
621
+ throw new CliUsageError(`Unable to read config file: ${configPath}`);
622
+ }
623
+ try {
624
+ const parsed = JSON.parse(raw);
625
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
626
+ throw new Error("Config must be a JSON object");
627
+ }
628
+ return normalizeCliConfig(parsed);
629
+ } catch {
630
+ throw new CliUsageError(`Invalid config file: ${configPath}`);
631
+ }
632
+ }
633
+ function normalizeCliConfig(config) {
634
+ return {
635
+ apiBaseUrl: stringValue(config.apiBaseUrl),
636
+ tokenEndpoint: stringValue(config.tokenEndpoint),
637
+ scope: stringValue(config.scope),
638
+ organisation: stringValue(config.organisation),
639
+ clientId: stringValue(config.clientId),
640
+ clientSecret: stringValue(config.clientSecret)
641
+ };
642
+ }
643
+ function stringValue(value) {
644
+ return typeof value === "string" && value.trim() ? value : void 0;
645
+ }
646
+ function resolveConfigPath(configPathOption, cwd) {
647
+ return resolve(cwd, configPathOption ?? DEFAULT_CONFIG_FILE);
648
+ }
649
+ function resolveTokenCachePath(tokenCachePathOption, cwd) {
650
+ return resolve(cwd, tokenCachePathOption ?? DEFAULT_TOKEN_CACHE_FILE);
651
+ }
652
+ function getCwd(deps) {
653
+ return deps.cwd ?? process.cwd();
654
+ }
655
+ function createFileTokenCache(path) {
656
+ return {
657
+ async read() {
658
+ if (!existsSync(path)) return null;
659
+ try {
660
+ const parsed = JSON.parse(await readFile2(path, "utf8"));
661
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
662
+ const value = parsed;
663
+ if (typeof value.accessToken !== "string" || typeof value.expiresAt !== "number" || typeof value.clientId !== "string" || typeof value.tokenEndpoint !== "string" || typeof value.scope !== "string") {
664
+ return null;
665
+ }
666
+ return {
667
+ accessToken: value.accessToken,
668
+ expiresAt: value.expiresAt,
669
+ clientId: value.clientId,
670
+ tokenEndpoint: value.tokenEndpoint,
671
+ scope: value.scope
672
+ };
673
+ } catch {
674
+ return null;
675
+ }
676
+ },
677
+ async write(entry) {
678
+ await mkdir(dirname(path), { recursive: true });
679
+ await writeFile(path, `${JSON.stringify(entry, null, 2)}
680
+ `, "utf8");
681
+ },
682
+ async clear() {
683
+ await rm(path, { force: true });
684
+ }
685
+ };
686
+ }
687
+ function createDefaultPrompt() {
688
+ if (!process.stdin.isTTY) {
689
+ let index = 0;
690
+ const linesPromise = readStdinLines();
691
+ return {
692
+ prompt: async (label, options = {}) => {
693
+ process.stdout.write(formatPrompt(label, options));
694
+ const lines = await linesPromise;
695
+ const answer = lines[index++] ?? "";
696
+ return firstNonEmpty(answer, options.defaultValue) ?? "";
697
+ },
698
+ close: () => void 0
699
+ };
700
+ }
701
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
702
+ return {
703
+ prompt: async (label, options = {}) => {
704
+ const answer = await rl.question(formatPrompt(label, options));
705
+ return firstNonEmpty(answer, options.defaultValue) ?? "";
706
+ },
707
+ close: () => rl.close()
708
+ };
709
+ }
710
+ function formatPrompt(label, options) {
711
+ const defaultHint = options.defaultValue && !options.secret ? ` [${options.defaultValue}]` : options.defaultValue && options.secret ? " [current value hidden]" : "";
712
+ return `${label}${defaultHint}: `;
713
+ }
714
+ async function readStdinLines() {
715
+ let input = "";
716
+ for await (const chunk of process.stdin) input += String(chunk);
717
+ return input.split(/\r?\n/);
718
+ }
719
+ function parseFiles(filesCsv) {
720
+ const files = filesCsv?.split(",").map((file) => file.trim()).filter(Boolean) ?? [];
721
+ if (files.length === 0) throw new CliUsageError("Missing files. Use --files <path[,path]>.");
722
+ return files;
723
+ }
724
+ function firstNonEmpty(...values) {
725
+ return values.map((value) => value?.trim()).find(Boolean);
726
+ }
727
+ function formatHumanResult(result) {
728
+ const lines = ["Docmana flow completed", `Status: ${String(result.status ?? "Unknown")}`];
729
+ const executionId = result.execution_id ?? result.executionResultId ?? result.execution_result_id;
730
+ if (executionId) lines.push(`Execution id: ${String(executionId)}`);
731
+ if (Array.isArray(result.results)) lines.push(`Results: ${result.results.length}`);
732
+ if (Array.isArray(result.errors)) lines.push(`Errors: ${result.errors.length}`);
733
+ const preview = formatResultsPreview(result.results);
734
+ if (preview) lines.push("", "Result preview:", preview);
735
+ lines.push("", "Use --json to print the complete result payload.");
736
+ return `${lines.join("\n")}
737
+ `;
738
+ }
739
+ function formatResultsPreview(results) {
740
+ if (!Array.isArray(results) || results.length === 0) return void 0;
741
+ const preview = JSON.stringify(results.slice(0, 3), null, 2);
742
+ if (!preview) return void 0;
743
+ return preview.length > 2e3 ? `${preview.slice(0, 2e3)}
744
+ ...` : preview;
745
+ }
746
+ function formatRuntimeError(err) {
747
+ if (err instanceof DocmanaExecutionError) {
748
+ const details = err.errors.length > 0 ? ` (${err.errors.length} error(s))` : "";
749
+ return `${err.message}${details}`;
750
+ }
751
+ if (err instanceof DocmanaError) return err.message;
752
+ if (err instanceof Error) return err.message;
753
+ return "Unexpected error";
754
+ }
755
+ function isDirectRun() {
756
+ const entry = process.argv[1];
757
+ if (!entry) return false;
758
+ try {
759
+ return realpathSync(resolve(entry)) === realpathSync(fileURLToPath(import.meta.url));
760
+ } catch {
761
+ return false;
762
+ }
763
+ }
764
+ if (isDirectRun()) {
765
+ runCli().then((code) => {
766
+ process.exitCode = code;
767
+ });
768
+ }
769
+ export {
770
+ runCli
771
+ };
772
+ //# sourceMappingURL=cli.mjs.map