@qoder-ai/qmind-cli 1.1.2 → 2.0.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.
package/dist/qmind.js CHANGED
@@ -3,17 +3,17 @@ import { z } from "zod";
3
3
  import { Command, CommanderError, Option } from "commander";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
5
  import { chmod, copyFile, link, lstat, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
- import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
7
- import { createReadStream, createWriteStream } from "node:fs";
8
6
  import { homedir } from "node:os";
9
- import pLimit from "p-limit";
10
- import picomatch from "picomatch";
11
- import open from "open";
7
+ import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
12
8
  import lockfile from "proper-lockfile";
9
+ import open from "open";
10
+ import { createReadStream, createWriteStream } from "node:fs";
13
11
  import { Readable } from "node:stream";
14
12
  import { pipeline } from "node:stream/promises";
15
13
  import { createParser } from "eventsource-parser";
16
14
  import { Agent, EnvHttpProxyAgent, request } from "undici";
15
+ import pLimit from "p-limit";
16
+ import picomatch from "picomatch";
17
17
  import { decodeHTML } from "entities";
18
18
  //#region ../sdk/src/errors.ts
19
19
  var QMindError = class extends Error {
@@ -47,12 +47,12 @@ const credentialHeaders = /* @__PURE__ */ new Set([
47
47
  "x-csrf-token",
48
48
  "x-xsrf-token"
49
49
  ]);
50
- function isRecord$4(value) {
50
+ function isRecord$5(value) {
51
51
  return typeof value === "object" && value !== null && !Array.isArray(value);
52
52
  }
53
53
  function nestedRecord(value, key) {
54
54
  const nested = value[key];
55
- return isRecord$4(nested) ? nested : void 0;
55
+ return isRecord$5(nested) ? nested : void 0;
56
56
  }
57
57
  function firstString(records, keys) {
58
58
  for (const record of records) {
@@ -78,7 +78,7 @@ function errorCodeForStatus$1(status) {
78
78
  }
79
79
  function httpError(access, operation, profile, response) {
80
80
  const body = profile.unwrapError(response.body);
81
- const root = isRecord$4(body) ? body : void 0;
81
+ const root = isRecord$5(body) ? body : void 0;
82
82
  const records = [
83
83
  root,
84
84
  root === void 0 ? void 0 : nestedRecord(root, "error"),
@@ -106,7 +106,7 @@ function httpError(access, operation, profile, response) {
106
106
  });
107
107
  }
108
108
  function isAbortLike(cause) {
109
- return isRecord$4(cause) && (cause.name === "AbortError" && typeof cause.name === "string" || cause.code === "ABORT_ERR");
109
+ return isRecord$5(cause) && (cause.name === "AbortError" && typeof cause.name === "string" || cause.code === "ABORT_ERR");
110
110
  }
111
111
  function transportError(access, operation, request, cause) {
112
112
  if (cause instanceof QMindError) return cause;
@@ -121,7 +121,11 @@ function requestWasAborted(request) {
121
121
  return request.signal?.aborted === true;
122
122
  }
123
123
  function assertWireRequestInvariant(request) {
124
- if (!Number.isFinite(request.timeoutMs ?? 1) || (request.timeoutMs ?? 1) <= 0) throw new QMindError("INVALID_ARGUMENT", "timeoutMs must be a positive finite number", { details: { field: "timeoutMs" } });
124
+ for (const [field, value] of [
125
+ ["connectTimeoutMs", request.connectTimeoutMs],
126
+ ["idleTimeoutMs", request.idleTimeoutMs],
127
+ ["timeoutMs", request.timeoutMs]
128
+ ]) if (!Number.isFinite(value ?? 1) || (value ?? 1) <= 0) throw new QMindError("INVALID_ARGUMENT", `${field} must be a positive finite number`, { details: { field } });
125
129
  if (request.destination.kind === "host") {
126
130
  if (!request.destination.path.startsWith("/") || request.destination.path.startsWith("//")) throw new QMindError("HOST_POLICY_ERROR", "host destination must be a root-relative path", { details: { destination: request.destination.kind } });
127
131
  return;
@@ -307,6 +311,10 @@ function normalizeQMindTaskRunStatus(status) {
307
311
  }
308
312
  //#endregion
309
313
  //#region ../sdk/src/schemas.ts
314
+ function isRecord$4(value) {
315
+ return typeof value === "object" && value !== null && !Array.isArray(value);
316
+ }
317
+ const wirePageNumberSchema = z.number().int().nonnegative().optional();
310
318
  const timestampSchema = z.union([z.string(), z.object({
311
319
  nanos: z.number().int().optional(),
312
320
  seconds: z.union([z.number(), z.string()])
@@ -347,26 +355,39 @@ const notebookSchema = z.object({
347
355
  userId: z.string().default("")
348
356
  });
349
357
  const notebookListSchema = z.object({
350
- currentPage: z.number().int().positive().optional(),
351
- notebooks: z.array(notebookSchema),
352
- pageSize: z.number().int().positive().optional(),
358
+ currentPage: wirePageNumberSchema,
359
+ notebooks: z.array(notebookSchema).nullish().transform((notebooks) => notebooks ?? []),
360
+ pageSize: wirePageNumberSchema,
353
361
  totalSize: z.number().int().nonnegative().default(0)
354
362
  });
355
- const wikiDocumentSchema = z.object({
363
+ const wikiDocumentSchema = z.preprocess((value) => isRecord$4(value) ? {
364
+ ...value,
365
+ contentHash: value.contentHash ?? value.content_hash,
366
+ contentUrl: value.contentUrl ?? value.content_url
367
+ } : value, z.object({
356
368
  contentHash: z.string().default(""),
357
369
  contentUrl: z.string().min(1),
358
370
  language: z.string().default("")
359
- });
360
- const wikiBranchSchema = z.object({
371
+ }));
372
+ const wikiBranchSchema = z.preprocess((value) => isRecord$4(value) ? {
373
+ ...value,
374
+ commitTimestamp: value.commitTimestamp ?? value.commit_timestamp,
375
+ knowledgeCount: value.knowledgeCount ?? value.knowledge_count,
376
+ repoUrl: value.repoUrl ?? value.repo_url,
377
+ resultVersion: value.resultVersion ?? value.result_version,
378
+ updatedAt: value.updatedAt ?? value.updated_at
379
+ } : value, z.object({
361
380
  branch: z.string().min(1),
362
381
  commitTimestamp: z.number().optional(),
363
- documents: z.array(wikiDocumentSchema).default([]),
382
+ documents: z.array(wikiDocumentSchema).nullish().transform((documents) => documents ?? []),
383
+ exists: z.boolean().optional(),
364
384
  knowledgeCount: z.number().int().nonnegative().default(0),
365
385
  repo: z.string().min(1),
366
386
  repoUrl: z.string().optional(),
367
- resultVersion: z.string().default("")
368
- });
369
- const wikiBranchListSchema = z.object({ branches: z.array(wikiBranchSchema) });
387
+ resultVersion: z.string().default(""),
388
+ updatedAt: timestampSchema.optional()
389
+ }));
390
+ const wikiBranchListSchema = z.object({ branches: z.array(wikiBranchSchema).nullish().transform((branches) => branches ?? []) });
370
391
  const cardLinkSchema = z.object({
371
392
  description: z.string().optional(),
372
393
  linkType: z.string().default(""),
@@ -374,6 +395,7 @@ const cardLinkSchema = z.object({
374
395
  targetTitle: z.string().optional()
375
396
  });
376
397
  const cardSchema = z.object({
398
+ branch: z.string().optional(),
377
399
  cardType: z.string().optional(),
378
400
  category: z.string().optional(),
379
401
  content: z.string().default(""),
@@ -382,6 +404,7 @@ const cardSchema = z.object({
382
404
  id: z.string().min(1),
383
405
  keywords: z.array(z.string()).default([]),
384
406
  knowledgeId: z.string().optional(),
407
+ language: z.string().optional(),
385
408
  links: z.array(cardLinkSchema).default([]),
386
409
  notebookId: z.string().optional(),
387
410
  orgId: z.string().optional(),
@@ -391,35 +414,58 @@ const cardSchema = z.object({
391
414
  title: z.string().default(""),
392
415
  updatedAt: timestampSchema.optional()
393
416
  });
417
+ const cardCategoryCountSchema = z.object({
418
+ category: z.string(),
419
+ count: z.number().int().nonnegative()
420
+ });
421
+ const cardLanguageCountSchema = z.object({
422
+ count: z.number().int().nonnegative(),
423
+ language: z.string()
424
+ });
425
+ const cardAppliedFiltersSchema = z.object({
426
+ branch: z.string().optional(),
427
+ category: z.string().optional(),
428
+ language: z.string().optional(),
429
+ query: z.string().optional(),
430
+ repo: z.string().optional(),
431
+ source: z.string().optional()
432
+ });
394
433
  const cardListSchema = z.object({
434
+ appliedFilters: cardAppliedFiltersSchema.optional(),
395
435
  cards: z.array(cardSchema).nullish().transform((cards) => cards ?? []),
396
- currentPage: z.number().int().positive().optional(),
397
- pageSize: z.number().int().positive().optional(),
436
+ categoryCounts: z.array(cardCategoryCountSchema).nullish().transform((counts) => counts ?? []),
437
+ currentPage: wirePageNumberSchema,
438
+ languageCounts: z.array(cardLanguageCountSchema).nullish().transform((counts) => counts ?? []),
439
+ pageSize: wirePageNumberSchema,
398
440
  totalSize: z.number().int().nonnegative().default(0)
399
441
  });
400
442
  const sourcePageInfoSchema = z.object({
401
- currentPage: z.number().int().positive().optional(),
402
- lastPage: z.number().int().positive().optional(),
403
- nextPage: z.number().int().positive().optional(),
443
+ currentPage: wirePageNumberSchema,
444
+ lastPage: wirePageNumberSchema,
445
+ nextPage: wirePageNumberSchema,
404
446
  nextToken: z.string().optional(),
405
- pageSize: z.number().int().positive().optional(),
406
- prevPage: z.number().int().positive().optional(),
447
+ pageSize: wirePageNumberSchema,
448
+ prevPage: wirePageNumberSchema,
407
449
  totalSize: z.number().int().nonnegative().optional()
408
450
  });
409
451
  const sourceSchema = z.object({
410
452
  childrenCount: z.number().int().nonnegative().optional(),
411
453
  createdAt: timestampSchema.optional(),
412
454
  errorMessage: z.string().default(""),
455
+ filename: z.string().optional(),
413
456
  hasChildren: z.boolean().optional(),
414
457
  id: z.string().min(1),
415
458
  isDir: z.boolean().default(false),
416
459
  metadata: z.record(z.string(), z.unknown()).nullable().optional(),
460
+ mimeType: z.string().optional(),
461
+ name: z.string().optional(),
417
462
  notebookId: z.string().default(""),
418
463
  orgId: z.string().default(""),
419
464
  originUrl: z.string().optional(),
420
465
  parentSourceId: z.string().default(""),
421
466
  path: z.string().default(""),
422
467
  sourceType: z.string().default(""),
468
+ size: z.number().int().nonnegative().optional(),
423
469
  status: z.string().default(""),
424
470
  title: z.string().default(""),
425
471
  updatedAt: timestampSchema.optional(),
@@ -427,10 +473,10 @@ const sourceSchema = z.object({
427
473
  userId: z.string().default("")
428
474
  });
429
475
  const sourceListSchema = z.object({
430
- currentPage: z.number().int().positive().optional(),
431
- pageSize: z.number().int().positive().optional(),
476
+ currentPage: wirePageNumberSchema,
477
+ pageSize: wirePageNumberSchema,
432
478
  pages: sourcePageInfoSchema.optional(),
433
- sources: z.array(sourceSchema),
479
+ sources: z.array(sourceSchema).nullish().transform((sources) => sources ?? []),
434
480
  totalSize: z.number().int().nonnegative().optional()
435
481
  });
436
482
  const sourceTreeNodeSchema = z.lazy(() => z.object({
@@ -449,6 +495,7 @@ const sourceTreeSchema = z.object({
449
495
  const sourceContentSchema = z.object({
450
496
  content: z.string(),
451
497
  contentType: z.string().default("text/markdown"),
498
+ size: z.number().int().nonnegative().optional(),
452
499
  sourceId: z.string().default("")
453
500
  });
454
501
  const sourceImportResultSchema = z.object({
@@ -474,10 +521,39 @@ const imageUrlsSchema = z.object({
474
521
  expiresAt: z.string().optional(),
475
522
  urls: z.record(z.string(), httpsUrlSchema)
476
523
  });
524
+ const notebookCitationSchema = z.preprocess((value) => isRecord$4(value) ? {
525
+ ...value,
526
+ chunkId: value.chunkId ?? value.chunk_id,
527
+ chunkIndex: value.chunkIndex ?? value.chunk_index,
528
+ originUrl: value.originUrl ?? value.origin_url,
529
+ sourceId: value.sourceId ?? value.source_id,
530
+ sourceTitle: value.sourceTitle ?? value.source_title,
531
+ sourceUri: value.sourceUri ?? value.source_uri
532
+ } : value, z.object({
533
+ chunkId: z.string().default(""),
534
+ chunkIndex: z.number().int().nonnegative().default(0),
535
+ metadata: z.record(z.string(), z.unknown()).optional(),
536
+ originUrl: z.string().optional(),
537
+ snippet: z.string().default(""),
538
+ sourceId: z.string().default(""),
539
+ sourceTitle: z.string().default(""),
540
+ sourceUri: z.string().default("")
541
+ }));
477
542
  const chatMessageSchema = z.object({
543
+ chatSessionId: z.string().optional(),
544
+ citations: z.array(notebookCitationSchema).nullish().transform((citations) => citations === null ? [] : citations),
478
545
  content: z.string().default(""),
546
+ createdAt: timestampSchema.optional(),
479
547
  id: z.string().min(1),
480
- role: z.string().default("")
548
+ metadata: z.record(z.string(), z.unknown()).optional(),
549
+ model: z.string().optional(),
550
+ notebookId: z.string().optional(),
551
+ orgId: z.string().optional(),
552
+ role: z.string().default(""),
553
+ sceneType: z.string().optional(),
554
+ sequence: z.number().int().nonnegative().optional(),
555
+ updatedAt: timestampSchema.optional(),
556
+ userId: z.string().optional()
481
557
  });
482
558
  const streamWireEventSchema = z.object({
483
559
  assistantMessage: chatMessageSchema.optional(),
@@ -522,16 +598,6 @@ const chatSessionSchema = z.object({
522
598
  updatedAt: timestampSchema.optional(),
523
599
  userId: z.string().default("")
524
600
  });
525
- const notebookCitationSchema = z.object({
526
- chunkId: z.string().default(""),
527
- chunkIndex: z.number().int().nonnegative().default(0),
528
- metadata: z.record(z.string(), z.unknown()).optional(),
529
- originUrl: z.string().optional(),
530
- snippet: z.string().default(""),
531
- sourceId: z.string().default(""),
532
- sourceTitle: z.string().default(""),
533
- sourceUri: z.string().default("")
534
- });
535
601
  const notebookContextChunkSchema = z.object({
536
602
  chunkId: z.string().default(""),
537
603
  chunkIndex: z.number().int().nonnegative().default(0),
@@ -586,6 +652,7 @@ const taskRunSchema = z.object({
586
652
  reportMarkdown: z.string().optional(),
587
653
  sceneType: z.string().default(""),
588
654
  scheduledTaskId: z.string().optional(),
655
+ sourceId: z.string().optional(),
589
656
  startedAt: timestampSchema.optional(),
590
657
  stats: z.record(z.string(), z.unknown()).optional(),
591
658
  status: z.string().default(""),
@@ -598,9 +665,9 @@ const taskRunSchema = z.object({
598
665
  status: normalizeQMindTaskRunStatus(value.status)
599
666
  }));
600
667
  const taskRunListSchema = z.object({
601
- currentPage: z.number().int().positive().optional(),
602
- pageSize: z.number().int().positive().optional(),
603
- runs: z.array(taskRunSchema).default([]),
668
+ currentPage: wirePageNumberSchema,
669
+ pageSize: wirePageNumberSchema,
670
+ runs: z.array(taskRunSchema).nullish().transform((runs) => runs ?? []),
604
671
  totalSize: z.number().int().nonnegative().default(0)
605
672
  });
606
673
  //#endregion
@@ -806,6 +873,8 @@ function taskRunBody(input) {
806
873
  config: optionalRecord(input.config, "config"),
807
874
  parentRunId: optionalTrimmedString(input.parentRunId, "parentRunId"),
808
875
  sceneType: requiredString$2(input.sceneType, "sceneType"),
876
+ settings: optionalRecord(input.settings, "settings"),
877
+ sourceId: optionalTrimmedString(input.sourceId, "sourceId"),
809
878
  scheduledTaskId: optionalTrimmedString(input.scheduledTaskId, "scheduledTaskId"),
810
879
  trigger: optionalTrimmedString(input.trigger, "trigger")
811
880
  });
@@ -826,6 +895,7 @@ function taskRunListPath(context, notebookId, input = {}) {
826
895
  ["scheduled_task_id", optionalTrimmedString(input.scheduledTaskId, "scheduledTaskId")],
827
896
  ["parent_run_id", optionalTrimmedString(input.parentRunId, "parentRunId")],
828
897
  ["scene_type", optionalTrimmedString(input.sceneType, "sceneType")],
898
+ ["source_id", optionalTrimmedString(input.sourceId, "sourceId")],
829
899
  ["trigger", optionalTrimmedString(input.trigger, "trigger")],
830
900
  ["status", status],
831
901
  ["page", normalizedPage.page],
@@ -853,6 +923,13 @@ function callOptions(options) {
853
923
  ...options?.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
854
924
  };
855
925
  }
926
+ function streamCallOptions(options) {
927
+ return {
928
+ ...callOptions(options),
929
+ ...options?.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: options.connectTimeoutMs },
930
+ ...options?.idleTimeoutMs === void 0 ? {} : { idleTimeoutMs: options.idleTimeoutMs }
931
+ };
932
+ }
856
933
  function createAgentOperations(context) {
857
934
  const createChatSession = (notebookId, input, options) => {
858
935
  const notebook = requiredString$2(notebookId, "notebookId");
@@ -885,7 +962,7 @@ function createAgentOperations(context) {
885
962
  },
886
963
  idempotent: false,
887
964
  method: "POST",
888
- ...callOptions(options)
965
+ ...streamCallOptions(options)
889
966
  },
890
967
  transport: context.transport
891
968
  });
@@ -1056,8 +1133,7 @@ function createNotebookOperations(context) {
1056
1133
  client: context.client,
1057
1134
  description,
1058
1135
  settings,
1059
- title,
1060
- userId: context.userId.length === 0 ? void 0 : context.userId
1136
+ title
1061
1137
  }),
1062
1138
  callOptions: options,
1063
1139
  capability: "notebooks.write",
@@ -1197,15 +1273,18 @@ function buildSourceTree(sources) {
1197
1273
  function sourceBody(input) {
1198
1274
  return compactUndefined({
1199
1275
  isDir: optionalBoolean(input?.isDir, "isDir"),
1276
+ metadata: optionalRecord(input?.metadata, "metadata"),
1200
1277
  parentSourceId: optionalString$1(input?.parentSourceId, "parentSourceId"),
1201
1278
  path: optionalString$1(input?.path, "path"),
1202
1279
  sourceType: optionalTrimmedString(input?.sourceType, "sourceType"),
1203
1280
  status: optionalTrimmedString(input?.status, "status"),
1204
- title: requiredString$2(input?.title, "title")
1281
+ title: requiredString$2(input?.title, "title"),
1282
+ uri: optionalString$1(input?.uri, "uri")
1205
1283
  });
1206
1284
  }
1207
1285
  function sourcePatch(input) {
1208
1286
  const patch = compactUndefined({
1287
+ content: optionalString$1(input?.content, "content"),
1209
1288
  errorMessage: optionalString$1(input?.errorMessage, "errorMessage"),
1210
1289
  isDir: optionalBoolean(input?.isDir, "isDir"),
1211
1290
  metadata: optionalRecord(input?.metadata, "metadata"),
@@ -1271,6 +1350,7 @@ function createSourceOperations(context) {
1271
1350
  const readSourcePage = async (notebookId, input = {}, options, operation = "sources.list") => {
1272
1351
  const notebook = requiredString$2(notebookId, "notebookId");
1273
1352
  const normalizedPage = pagination$1(input);
1353
+ const isDir = optionalBoolean(input.isDir, "isDir");
1274
1354
  const view = input.view ?? "list";
1275
1355
  if (view !== "list" && view !== "children") throw invalidArgument$1("view must be either \"list\" or \"children\"", "view");
1276
1356
  const parentSourceId = optionalString$1(input.parentSourceId, "parentSourceId");
@@ -1286,6 +1366,7 @@ function createSourceOperations(context) {
1286
1366
  operation,
1287
1367
  path: queryPath(context.profile.apiPath("notebooks", notebook, "sources"), [
1288
1368
  ["view", view === "list" ? void 0 : view],
1369
+ ["is_dir", isDir === void 0 ? void 0 : String(isDir)],
1289
1370
  ["parent_source_id", parentSourceId],
1290
1371
  ["page", normalizedPage.page],
1291
1372
  ["page_size", normalizedPage.pageSize],
@@ -1703,21 +1784,21 @@ function createTransferOperations(context) {
1703
1784
  //#endregion
1704
1785
  //#region ../sdk/src/capabilities.ts
1705
1786
  const dashboardCapabilities = Object.freeze({
1706
- botGateway: true,
1787
+ botGateway: false,
1707
1788
  "cards.read": true,
1708
1789
  "cards.write": true,
1709
1790
  chat: true,
1710
- "featuredNotebooks.read": true,
1791
+ "featuredNotebooks.read": false,
1711
1792
  "images.presignUpload": false,
1712
1793
  "images.presignUrls": false,
1713
- members: true,
1794
+ members: false,
1714
1795
  "notebooks.filterByClient": false,
1715
1796
  "notebooks.read": true,
1716
1797
  "notebooks.write": true,
1717
- "notes.read": true,
1718
- "notes.write": true,
1798
+ "notes.read": false,
1799
+ "notes.write": false,
1719
1800
  retrieval: true,
1720
- scheduledTasks: true,
1801
+ scheduledTasks: false,
1721
1802
  "sources.children": true,
1722
1803
  "sources.importWeb": true,
1723
1804
  "sources.read": true,
@@ -1734,17 +1815,17 @@ const sashCapabilities = Object.freeze({
1734
1815
  "cards.read": true,
1735
1816
  "cards.write": true,
1736
1817
  chat: true,
1737
- "featuredNotebooks.read": true,
1818
+ "featuredNotebooks.read": false,
1738
1819
  "images.presignUpload": true,
1739
1820
  "images.presignUrls": true,
1740
- members: true,
1821
+ members: false,
1741
1822
  "notebooks.filterByClient": true,
1742
1823
  "notebooks.read": true,
1743
1824
  "notebooks.write": true,
1744
- "notes.read": true,
1745
- "notes.write": true,
1825
+ "notes.read": false,
1826
+ "notes.write": false,
1746
1827
  retrieval: true,
1747
- scheduledTasks: true,
1828
+ scheduledTasks: false,
1748
1829
  "sources.children": false,
1749
1830
  "sources.importWeb": false,
1750
1831
  "sources.read": true,
@@ -1958,8 +2039,7 @@ function createQMindClient(options) {
1958
2039
  const context = {
1959
2040
  client: optionalTrimmedString(options.client, "client") ?? "qmind",
1960
2041
  profile,
1961
- transport: options.transport,
1962
- userId: optionalTrimmedString(options.userId, "userId") ?? ""
2042
+ transport: options.transport
1963
2043
  };
1964
2044
  return Object.freeze({
1965
2045
  access: profile.access,
@@ -2024,7 +2104,7 @@ function requestsJsonOutput(argv) {
2024
2104
  }
2025
2105
  //#endregion
2026
2106
  //#region src/build-info.ts
2027
- const QMIND_CLI_VERSION = "1.1.2";
2107
+ const QMIND_CLI_VERSION = "2.0.0";
2028
2108
  const QMIND_CLI_BUILD_TIME = "development";
2029
2109
  const QMIND_CLI_UPDATE_BASE_URL = typeof __QMIND_CLI_UPDATE_BASE_URL__ === "string" ? __QMIND_CLI_UPDATE_BASE_URL__ : "https://qoder-ide-cn.oss-cn-hangzhou.aliyuncs.com/qmind/cli/";
2030
2110
  function qmindCliPlatform() {
@@ -2034,1562 +2114,1598 @@ function qmindCliArchitecture() {
2034
2114
  return process.arch === "x64" ? "amd64" : process.arch;
2035
2115
  }
2036
2116
  //#endregion
2037
- //#region src/files.ts
2038
- function safeQMindDownloadName(title, fallback) {
2039
- const normalized = [...title.normalize("NFKC")].map((character) => {
2040
- const codePoint = character.codePointAt(0) ?? 0;
2041
- return codePoint <= 31 || codePoint === 127 ? "_" : character;
2042
- }).join("").replace(/[\\/]/g, "_").replace(/^\.+$/, "").trim().slice(0, 240);
2043
- const safe = basename(normalized);
2044
- if (safe.length > 0 && safe !== "." && safe !== "..") return safe;
2045
- return basename(fallback.replace(/[\\/]/g, "_")) || "download";
2117
+ //#region ../node-host/src/auth.ts
2118
+ const responseRecordSchema = z.record(z.string(), z.unknown());
2119
+ const personalTokenExchangeSchema = z.object({ token: z.string().min(1) });
2120
+ const devicePollSchema = z.object({
2121
+ expires_at: z.string().default(""),
2122
+ refresh_token: z.string().default(""),
2123
+ refresh_token_expires_at: z.string().default(""),
2124
+ token: z.string().min(1)
2125
+ });
2126
+ const refreshSchema = z.object({
2127
+ device_token: z.string().min(1),
2128
+ expires_at: z.string().default(""),
2129
+ refresh_token: z.string().optional(),
2130
+ refresh_token_expires_at: z.string().optional()
2131
+ });
2132
+ const POLL_INTERVAL_MS = 2e3;
2133
+ const LOGIN_TIMEOUT_MS = 3e5;
2134
+ const EXPIRY_SKEW_MS = 3e4;
2135
+ function base64Url(bytes) {
2136
+ return Buffer.from(bytes).toString("base64url");
2046
2137
  }
2047
- async function commitQMindTemporaryFile(temporary, path, overwrite) {
2048
- if (!overwrite) {
2049
- await link(temporary, path).catch((error) => {
2050
- if (error.code === "EEXIST") throw new QMindError("CONFLICT", `target already exists: ${path}`);
2051
- throw error;
2052
- });
2053
- await rm(temporary, { force: true });
2054
- return;
2055
- }
2056
- if (process.platform !== "win32") {
2057
- await rename(temporary, path);
2058
- return;
2059
- }
2060
- const backup = `${path}.${randomBytes(8).toString("hex")}.old`;
2061
- let backedUp = false;
2062
- try {
2063
- try {
2064
- await rename(path, backup);
2065
- backedUp = true;
2066
- } catch (error) {
2067
- if (error.code !== "ENOENT") throw error;
2068
- }
2069
- await rename(temporary, path);
2070
- if (backedUp) await rm(backup, { force: true });
2071
- } catch (error) {
2072
- if (backedUp) await rename(backup, path).catch(() => void 0);
2073
- throw error;
2074
- }
2138
+ function createPkce() {
2139
+ const verifier = base64Url(randomBytes(32));
2140
+ return {
2141
+ challenge: createHash("sha256").update(verifier).digest("base64url"),
2142
+ nonce: base64Url(randomBytes(16)),
2143
+ verifier
2144
+ };
2075
2145
  }
2076
- /** Atomic write with a no-clobber default. Temporary files are always cleaned. */
2077
- async function atomicQMindWrite(path, content, overwrite = false) {
2078
- await mkdir(dirname(path), { recursive: true });
2079
- const temporary = join(dirname(path), `.${basename(path)}.${randomBytes(8).toString("hex")}.tmp`);
2080
- try {
2081
- await writeFile(temporary, content, {
2082
- flag: "wx",
2083
- mode: 420
2084
- });
2085
- await commitQMindTemporaryFile(temporary, path, overwrite);
2086
- } catch (error) {
2087
- await rm(temporary, { force: true });
2088
- throw error;
2089
- }
2146
+ function errorCodeForStatus(status) {
2147
+ if (status === 401 || status === 403) return "AUTH_REQUIRED";
2148
+ if (status === 404) return "NOT_FOUND";
2149
+ if (status === 409) return "CONFLICT";
2150
+ if (status === 429) return "RATE_LIMITED";
2151
+ if (status >= 400 && status < 500) return "INVALID_ARGUMENT";
2152
+ return "REMOTE_ERROR";
2090
2153
  }
2091
- //#endregion
2092
- //#region src/folder-sync.ts
2093
- const QMIND_FOLDER_SYNC_DEFAULT_EXTENSIONS = Object.freeze([
2094
- ".mdx",
2095
- ".md",
2096
- ".pdf",
2097
- ".txt",
2098
- ".docx",
2099
- ".pptx",
2100
- ".png",
2101
- ".jpg",
2102
- ".jpeg",
2103
- ".gif",
2104
- ".svg",
2105
- ".webp"
2106
- ]);
2107
- function posix(path) {
2108
- return path.split(sep).join("/");
2154
+ function record$1(value) {
2155
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2109
2156
  }
2110
- function qmindFolderSyncIndexKey(parentId, title) {
2111
- return `${parentId}\u0000${title}`;
2157
+ function responseString(response, ...keys) {
2158
+ const body = record$1(response.body);
2159
+ const nestedError = record$1(body?.error);
2160
+ for (const source of [body, nestedError]) for (const key of keys) {
2161
+ const value = source?.[key];
2162
+ if (typeof value === "string" && value.length > 0) return value;
2163
+ }
2112
2164
  }
2113
- function normalizeExtensions(values) {
2114
- return new Set(values.map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0).map((value) => value.startsWith(".") ? value : `.${value}`));
2165
+ function header(response, name) {
2166
+ const normalized = name.toLowerCase();
2167
+ for (const [key, value] of Object.entries(response.headers)) if (key.toLowerCase() === normalized && value.length > 0) return value;
2115
2168
  }
2116
- function validateMapping(value) {
2117
- if (value === null || typeof value !== "object" || Array.isArray(value)) throw new QMindError("INVALID_ARGUMENT", "directory ID mapping must be a JSON object");
2118
- const result = {};
2119
- for (const [path, id] of Object.entries(value)) {
2120
- if (typeof id !== "string" || id.trim().length === 0) throw new QMindError("INVALID_ARGUMENT", "directory ID mapping values must be non-empty strings");
2121
- result[posix(path)] = id.trim();
2169
+ function authResponseError(response, operation) {
2170
+ const requestId = responseString(response, "requestId", "request_id") ?? header(response, "x-request-id");
2171
+ const upstreamCode = responseString(response, "errorCode", "error_code", "code");
2172
+ return new QMindError(errorCodeForStatus(response.status), `${operation} failed`, {
2173
+ details: { operation },
2174
+ ...requestId === void 0 ? {} : { requestId },
2175
+ status: response.status,
2176
+ ...upstreamCode === void 0 ? {} : { upstreamCode }
2177
+ });
2178
+ }
2179
+ async function authRequest(transport, method, path, operation, body) {
2180
+ const request = {
2181
+ ...body === void 0 ? {} : { body: {
2182
+ kind: "json",
2183
+ value: body
2184
+ } },
2185
+ destination: {
2186
+ kind: "host",
2187
+ path
2188
+ },
2189
+ idempotent: method === "GET",
2190
+ method
2191
+ };
2192
+ let response;
2193
+ try {
2194
+ response = await transport.request(request);
2195
+ } catch (cause) {
2196
+ if (cause instanceof QMindError) throw cause;
2197
+ throw new QMindError("NETWORK_ERROR", `${operation} failed`);
2122
2198
  }
2123
- return result;
2199
+ if (response.status < 200 || response.status >= 300) throw authResponseError(response, operation);
2200
+ const parsed = responseRecordSchema.safeParse(response.body);
2201
+ if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", `${operation} returned an invalid response`, {
2202
+ details: { operation },
2203
+ status: response.status
2204
+ });
2205
+ return parsed.data;
2124
2206
  }
2125
- async function sha256(path) {
2126
- const hash = createHash("sha256");
2127
- for await (const chunk of createReadStream(path)) hash.update(chunk);
2128
- return hash.digest("hex");
2207
+ function credentialsFromLogin(value, sashOrigin) {
2208
+ return {
2209
+ deviceToken: value.token,
2210
+ expiresAt: value.expires_at,
2211
+ refreshToken: value.refresh_token,
2212
+ refreshTokenExpiresAt: value.refresh_token_expires_at,
2213
+ sashOrigin,
2214
+ savedAt: "",
2215
+ schemaVersion: 2
2216
+ };
2129
2217
  }
2130
- function createNodeQMindFolderSyncFileSystem() {
2218
+ function isExpired(timestamp, now, skew = 0) {
2219
+ if (timestamp.length === 0) return false;
2220
+ const value = Date.parse(timestamp);
2221
+ return Number.isFinite(value) && value <= now + skew;
2222
+ }
2223
+ function aborted(signal) {
2224
+ return signal?.aborted === true ? new QMindError("ABORTED", "QMind authorization was aborted") : void 0;
2225
+ }
2226
+ function createSystemClock() {
2131
2227
  return Object.freeze({
2132
- async isDirectory(path) {
2133
- return (await stat(path).catch(() => void 0))?.isDirectory() === true;
2134
- },
2135
- async readMapping(path) {
2136
- try {
2137
- return validateMapping(JSON.parse(await readFile(path, "utf8")));
2138
- } catch (error) {
2139
- if (error instanceof QMindError) throw error;
2140
- throw new QMindError("INVALID_ARGUMENT", `unable to read directory ID mapping: ${path}`);
2141
- }
2142
- },
2143
- async scan(baseDirectory, options) {
2144
- const directories = [];
2145
- const files = [];
2146
- let skipped = 0;
2147
- const isIgnored = options.ignorePatterns.length === 0 ? () => false : picomatch([...options.ignorePatterns], { dot: true });
2148
- const visit = async (directory) => {
2149
- const entries = await readdir(directory, { withFileTypes: true });
2150
- entries.sort((left, right) => left.name.localeCompare(right.name));
2151
- for (const entry of entries) {
2152
- const absolute = join(directory, entry.name);
2153
- const path = posix(relative(baseDirectory, absolute));
2154
- if (entry.name.startsWith(".") || isIgnored(path)) {
2155
- skipped += entry.isFile() ? 1 : 0;
2156
- continue;
2157
- }
2158
- if (entry.isDirectory()) {
2159
- directories.push(path);
2160
- await visit(absolute);
2161
- } else if (entry.isFile()) {
2162
- if (options.extensions.has(extname(entry.name).toLowerCase())) files.push(path);
2163
- else skipped += 1;
2164
- }
2228
+ now: Date.now,
2229
+ sleep(milliseconds, signal) {
2230
+ return new Promise((resolve, reject) => {
2231
+ const alreadyAborted = aborted(signal);
2232
+ if (alreadyAborted !== void 0) {
2233
+ reject(alreadyAborted);
2234
+ return;
2165
2235
  }
2166
- };
2167
- await visit(baseDirectory);
2168
- directories.sort((left, right) => left.split("/").length - right.split("/").length || left.localeCompare(right));
2169
- files.sort((left, right) => left.localeCompare(right));
2170
- return {
2171
- directories,
2172
- files,
2173
- skipped
2174
- };
2175
- },
2176
- sha256,
2177
- async writeMapping(path, mapping) {
2178
- await mkdir(dirname(path), { recursive: true });
2179
- await writeFile(path, `${JSON.stringify(mapping, null, 2)}\n`, { mode: 384 });
2236
+ const onAbort = () => {
2237
+ clearTimeout(timer);
2238
+ reject(new QMindError("ABORTED", "QMind authorization was aborted"));
2239
+ };
2240
+ const timer = setTimeout(() => {
2241
+ signal?.removeEventListener("abort", onAbort);
2242
+ resolve();
2243
+ }, milliseconds);
2244
+ signal?.addEventListener("abort", onAbort, { once: true });
2245
+ });
2180
2246
  }
2181
2247
  });
2182
2248
  }
2183
- async function shouldReplaceQMindFolderFile(mode, existing, localPath, fileSystem = createNodeQMindFolderSyncFileSystem()) {
2184
- if (mode === "skip") return false;
2185
- if (mode === "overwrite") return true;
2186
- const metadata = existing.metadata ?? {};
2187
- const remoteHash = metadata.fileSha256 ?? metadata.rawFileSha256 ?? metadata.sha256;
2188
- if (typeof remoteHash !== "string" || !/^[a-f0-9]{64}$/i.test(remoteHash)) return true;
2189
- return (await fileSystem.sha256(localPath)).toLowerCase() !== remoteHash.toLowerCase();
2249
+ function createQMindCliAuthManager(options) {
2250
+ let credentials = options.credentials;
2251
+ let explicitToken = options.explicitToken;
2252
+ let token = "";
2253
+ let refreshPromise;
2254
+ const login = async (loginOptions = {}) => {
2255
+ if (options.config.nonInteractive) throw new QMindError("AUTH_REQUIRED", "login requires an interactive terminal");
2256
+ const pkce = createPkce();
2257
+ const browserUrl = new URL("/device/selectAccounts", options.config.dashboardOrigin);
2258
+ browserUrl.searchParams.set("nonce", pkce.nonce);
2259
+ browserUrl.searchParams.set("challenge", pkce.challenge);
2260
+ browserUrl.searchParams.set("challenge_method", "S256");
2261
+ browserUrl.searchParams.set("machine_id", pkce.nonce);
2262
+ if (loginOptions.clientId !== void 0 && loginOptions.clientId.trim().length > 0) browserUrl.searchParams.set("client_id", loginOptions.clientId.trim());
2263
+ options.reporter.info(`Opening browser for authorization. If it does not open, visit:\n${browserUrl.toString()}`);
2264
+ if (loginOptions.openBrowser !== false) try {
2265
+ await options.browser.open(browserUrl.toString());
2266
+ } catch {
2267
+ options.reporter.warn("warning: unable to open a browser; use the authorization URL above");
2268
+ }
2269
+ options.reporter.info("Waiting for authorization (timeout: 5m)...");
2270
+ const deadline = options.clock.now() + LOGIN_TIMEOUT_MS;
2271
+ while (options.clock.now() < deadline) {
2272
+ const abortError = aborted(loginOptions.signal);
2273
+ if (abortError !== void 0) throw abortError;
2274
+ await options.clock.sleep(POLL_INTERVAL_MS, loginOptions.signal);
2275
+ const pollPath = `/api/v1/deviceToken/poll?${new URLSearchParams({
2276
+ challenge_method: "S256",
2277
+ nonce: pkce.nonce,
2278
+ verifier: pkce.verifier
2279
+ }).toString()}`;
2280
+ try {
2281
+ const raw = await authRequest(options.transport, "GET", pollPath, "device authorization polling");
2282
+ const parsed = devicePollSchema.safeParse(raw);
2283
+ if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "device authorization returned an invalid response");
2284
+ credentials = credentialsFromLogin(parsed.data, options.config.sashOrigin);
2285
+ await options.vault.save(credentials);
2286
+ explicitToken = "";
2287
+ token = credentials.deviceToken;
2288
+ return {
2289
+ expiresAt: credentials.expiresAt,
2290
+ sashOrigin: credentials.sashOrigin
2291
+ };
2292
+ } catch (error) {
2293
+ if (error instanceof QMindError && error.status === 404) continue;
2294
+ throw error;
2295
+ }
2296
+ }
2297
+ throw new QMindError("AUTH_REQUIRED", "authorization timed out after 5m");
2298
+ };
2299
+ const refreshInternal = async (reason) => {
2300
+ if (credentials === void 0 || credentials.refreshToken.length === 0) {
2301
+ token = "";
2302
+ throw new QMindError("AUTH_REQUIRED", `credentials cannot be refreshed after ${reason}`);
2303
+ }
2304
+ if (isExpired(credentials.refreshTokenExpiresAt, options.clock.now())) {
2305
+ await options.vault.deleteIf(credentials);
2306
+ credentials = void 0;
2307
+ token = "";
2308
+ throw new QMindError("AUTH_REQUIRED", "stored refresh credentials have expired");
2309
+ }
2310
+ const raw = await authRequest(options.transport, "POST", "/api/v1/deviceToken/refresh", "credential refresh", { refresh_token: credentials.refreshToken });
2311
+ const parsed = refreshSchema.safeParse(raw);
2312
+ if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "credential refresh returned an invalid response");
2313
+ const basis = credentials;
2314
+ const legacy = basis.schemaVersion !== 2;
2315
+ const refreshed = {
2316
+ ...basis,
2317
+ deviceToken: parsed.data.device_token,
2318
+ expiresAt: parsed.data.expires_at,
2319
+ refreshToken: parsed.data.refresh_token ?? basis.refreshToken,
2320
+ refreshTokenExpiresAt: parsed.data.refresh_token_expires_at ?? basis.refreshTokenExpiresAt,
2321
+ sashOrigin: options.config.sashOrigin,
2322
+ schemaVersion: 2
2323
+ };
2324
+ const current = await options.vault.compareAndSwap(basis, refreshed, { migrateLegacy: legacy }) ? refreshed : await options.vault.load();
2325
+ if (current === void 0) {
2326
+ credentials = void 0;
2327
+ token = "";
2328
+ throw new QMindError("AUTH_REQUIRED", "credentials changed during refresh");
2329
+ }
2330
+ credentials = current;
2331
+ token = current.deviceToken;
2332
+ return token;
2333
+ };
2334
+ const manager = {
2335
+ async getToken() {
2336
+ if (token.length > 0) return token;
2337
+ const candidate = explicitToken || credentials?.deviceToken || "";
2338
+ if (candidate.length === 0) {
2339
+ if (options.config.nonInteractive) throw new QMindError("AUTH_REQUIRED", "no credentials found; run 'qmind login' in an interactive terminal");
2340
+ await login();
2341
+ return token;
2342
+ }
2343
+ if (candidate.startsWith("pt-")) {
2344
+ const raw = await authRequest(options.transport, "POST", "/api/v1/jobToken/exchange", "personal token exchange", { personal_token: candidate });
2345
+ const parsed = personalTokenExchangeSchema.safeParse(raw);
2346
+ if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "personal token exchange returned an invalid response");
2347
+ token = parsed.data.token;
2348
+ return token;
2349
+ }
2350
+ if (credentials !== void 0 && candidate === credentials.deviceToken && isExpired(credentials.expiresAt, options.clock.now(), EXPIRY_SKEW_MS)) return manager.refreshToken("expired", candidate);
2351
+ token = candidate;
2352
+ return token;
2353
+ },
2354
+ login,
2355
+ async logout() {
2356
+ await options.vault.delete();
2357
+ credentials = void 0;
2358
+ explicitToken = "";
2359
+ token = "";
2360
+ refreshPromise = void 0;
2361
+ },
2362
+ async refreshToken(reason, rejectedToken) {
2363
+ if (rejectedToken !== void 0 && token.length > 0 && token !== rejectedToken) return token;
2364
+ refreshPromise ??= refreshInternal(reason).finally(() => {
2365
+ refreshPromise = void 0;
2366
+ });
2367
+ return await refreshPromise;
2368
+ }
2369
+ };
2370
+ return Object.freeze(manager);
2190
2371
  }
2191
- function retryable(error) {
2192
- if (!(error instanceof QMindError)) return true;
2193
- if (error.code === "FILE_TOO_LARGE" || error.code === "INVALID_ARGUMENT" || error.code === "AUTH_REQUIRED" || error.code === "FORBIDDEN") return false;
2194
- return error.status === void 0 || error.status === 408 || error.status === 429 || error.status >= 500;
2372
+ //#endregion
2373
+ //#region ../node-host/src/authenticated-transport.ts
2374
+ function withBearer(request, token) {
2375
+ return {
2376
+ ...request,
2377
+ headers: {
2378
+ ...request.headers,
2379
+ authorization: `Bearer ${token}`
2380
+ }
2381
+ };
2195
2382
  }
2196
- async function withUploadRetry(task, sleep) {
2197
- let lastFailure;
2198
- for (let attempt = 0; attempt < 3; attempt += 1) try {
2199
- return await task();
2200
- } catch (error) {
2201
- lastFailure = error;
2202
- if (!retryable(error) || attempt === 2) break;
2203
- await sleep(250 * 2 ** attempt);
2204
- }
2205
- throw lastFailure;
2383
+ function createAuthenticatedQMindTransport(transport, auth) {
2384
+ return Object.freeze({
2385
+ async request(request) {
2386
+ if (request.destination.kind === "signed") return await transport.request(request);
2387
+ const token = await auth.getToken();
2388
+ const response = await transport.request(withBearer(request, token));
2389
+ if (response.status !== 401) return response;
2390
+ const refreshed = await auth.refreshToken("unauthorized", token);
2391
+ return await transport.request(withBearer(request, refreshed));
2392
+ },
2393
+ async stream(request) {
2394
+ const token = await auth.getToken();
2395
+ const response = await transport.stream(withBearer(request, token));
2396
+ if (response.kind !== "response" || response.status !== 401) return response;
2397
+ const refreshed = await auth.refreshToken("unauthorized", token);
2398
+ return await transport.stream(withBearer(request, refreshed));
2399
+ }
2400
+ });
2206
2401
  }
2207
- function defaultSleep(milliseconds) {
2208
- return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
2402
+ //#endregion
2403
+ //#region ../node-host/src/config.ts
2404
+ const fileConfigSchema = z.object({
2405
+ client: z.string().optional(),
2406
+ dashboard_url: z.string().optional(),
2407
+ proxy_from_env: z.boolean().optional(),
2408
+ request_timeout_ms: z.number().int().positive().optional(),
2409
+ sash_url: z.string().optional(),
2410
+ stream_idle_timeout_ms: z.number().int().positive().optional(),
2411
+ tls_ca_file: z.string().optional(),
2412
+ tls_reject_unauthorized: z.boolean().optional(),
2413
+ update_public_key: z.string().optional()
2414
+ }).passthrough();
2415
+ const PRESETS = Object.freeze({
2416
+ daily: {
2417
+ dashboardOrigin: "https://daily.qoder.ai",
2418
+ sashOrigin: "https://daily-openapi.qoder.sh"
2419
+ },
2420
+ prod: {
2421
+ dashboardOrigin: "https://qoder.com",
2422
+ sashOrigin: "https://openapi.qoder.sh"
2423
+ },
2424
+ test: {
2425
+ dashboardOrigin: "https://test.qoder.ai",
2426
+ sashOrigin: "https://test-openapi.qoder.sh"
2427
+ }
2428
+ });
2429
+ function invalidConfig(message, field) {
2430
+ return new QMindError("INVALID_ARGUMENT", message, { ...field === void 0 ? {} : { details: { field } } });
2209
2431
  }
2210
- function mappingPath(baseDirectory, cacheDirectory) {
2211
- const cache = cacheDirectory ?? process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
2212
- const name = createHash("sha1").update(baseDirectory).digest("hex").slice(0, 16);
2213
- return join(cache, "qmind", "dir-ids", `${name}.json`);
2432
+ function normalizeOrigin$1(rawValue, field) {
2433
+ let url;
2434
+ try {
2435
+ url = new URL(rawValue);
2436
+ } catch {
2437
+ throw invalidConfig(`${field} must be a valid HTTP(S) origin`, field);
2438
+ }
2439
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw invalidConfig(`${field} must contain only an HTTP(S) scheme and authority`, field);
2440
+ return url.origin;
2214
2441
  }
2215
- function addFailure(stats, field, path) {
2216
- (stats[field] ??= []).push(path);
2442
+ function optionalTrimmed(value) {
2443
+ const trimmed = value?.trim();
2444
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
2217
2445
  }
2218
- function publicStats(stats) {
2219
- return Object.freeze({
2220
- ...stats,
2221
- ...stats.failed_dirs === void 0 ? {} : { failed_dirs: Object.freeze(stats.failed_dirs) },
2222
- ...stats.failed_files === void 0 ? {} : { failed_files: Object.freeze(stats.failed_files) },
2223
- ...stats.failed_deletes === void 0 ? {} : { failed_deletes: Object.freeze(stats.failed_deletes) }
2224
- });
2446
+ function parseBoolean(value, field) {
2447
+ const normalized = optionalTrimmed(value)?.toLowerCase();
2448
+ if (normalized === void 0) return void 0;
2449
+ if (normalized === "1" || normalized === "true") return true;
2450
+ if (normalized === "0" || normalized === "false") return false;
2451
+ throw invalidConfig(`${field} must be 1, 0, true, or false`, field);
2225
2452
  }
2226
- async function synchronizeQMindFolder(host, options) {
2227
- const notebookId = options.notebookId.trim();
2228
- if (notebookId.length === 0) throw new QMindError("INVALID_ARGUMENT", "notebookId is required");
2229
- const fileSystem = options.fileSystem ?? createNodeQMindFolderSyncFileSystem();
2230
- const baseDirectory = resolve(options.baseDirectory);
2231
- if (!await fileSystem.isDirectory(baseDirectory)) throw new QMindError("INVALID_ARGUMENT", `${baseDirectory} is not a directory`);
2232
- const mode = options.mode ?? "skip";
2233
- if (options.skipUpload === true && options.delete === true) throw new QMindError("INVALID_ARGUMENT", "delete cannot be combined with skipUpload because local files would not enter the mirror plan");
2234
- const requestedConcurrency = options.concurrency ?? 3;
2235
- if (!Number.isSafeInteger(requestedConcurrency) || requestedConcurrency < 1) throw new QMindError("INVALID_ARGUMENT", "concurrency must be a positive integer");
2236
- const concurrency = Math.min(10, requestedConcurrency);
2237
- const diagnostics = [];
2238
- if (requestedConcurrency > 10) diagnostics.push(`warning: concurrency ${requestedConcurrency} exceeds maximum (10); clamped to 10`);
2239
- const extensions = normalizeExtensions(options.extensions ?? QMIND_FOLDER_SYNC_DEFAULT_EXTENSIONS);
2240
- const tree = await fileSystem.scan(baseDirectory, {
2241
- extensions,
2242
- ignorePatterns: options.ignorePatterns ?? []
2243
- });
2244
- const remote = await host.client.listAllSources(notebookId);
2245
- const remoteIndex = new Map(remote.map((source) => [qmindFolderSyncIndexKey(source.parentSourceId, source.title), source]));
2246
- const remoteById = new Map(remote.map((source) => [source.id, source]));
2247
- const stats = {
2248
- dirs_created: 0,
2249
- dirs_reused: 0,
2250
- dirs_failed: 0,
2251
- dirs_refresh_failed: 0,
2252
- files_uploaded: 0,
2253
- files_skipped: options.skipUpload === true ? 0 : tree.skipped,
2254
- files_already_exist: 0,
2255
- files_failed: 0,
2256
- ...options.delete === true ? {
2257
- delete_failed: 0,
2258
- dirs_deleted: 0,
2259
- files_deleted: 0
2260
- } : {},
2261
- ...options.dryRun === true ? { dry_run: true } : {}
2262
- };
2263
- const configuredMapping = options.directoryIdsPath === void 0 ? {} : await fileSystem.readMapping(options.directoryIdsPath);
2264
- const directoryIds = {};
2265
- const desiredKeys = /* @__PURE__ */ new Set();
2266
- for (const directory of tree.directories) {
2267
- const parentPath = posix(dirname(directory)) === "." ? "" : posix(dirname(directory));
2268
- const parentId = directoryIds[parentPath] ?? "";
2269
- if (parentPath.length > 0 && parentId.length === 0) {
2270
- stats.dirs_failed += 1;
2271
- addFailure(stats, "failed_dirs", directory);
2272
- continue;
2273
- }
2274
- const title = basename(directory);
2275
- const key = qmindFolderSyncIndexKey(parentId, title);
2276
- desiredKeys.add(key);
2277
- const mappedId = configuredMapping[directory];
2278
- const mapped = mappedId === void 0 ? void 0 : remoteById.get(mappedId);
2279
- const existing = mappedId === void 0 || mapped === void 0 ? remoteIndex.get(key) : mapped;
2280
- if (existing?.isDir === true && existing.parentSourceId === parentId && existing.title === title) {
2281
- directoryIds[directory] = existing.id;
2282
- stats.dirs_reused += 1;
2283
- continue;
2284
- }
2285
- if (existing !== void 0) {
2286
- stats.dirs_failed += 1;
2287
- addFailure(stats, "failed_dirs", directory);
2288
- continue;
2289
- }
2290
- if (options.dryRun === true) {
2291
- directoryIds[directory] = `dry-run:${directory}`;
2292
- stats.dirs_created += 1;
2293
- continue;
2294
- }
2295
- try {
2296
- const created = await host.client.createDirectory(notebookId, {
2297
- parentSourceId: parentId,
2298
- path: title,
2299
- title
2300
- });
2301
- directoryIds[directory] = created.id;
2302
- remoteIndex.set(key, created);
2303
- remoteById.set(created.id, created);
2304
- stats.dirs_created += 1;
2305
- } catch {
2306
- stats.dirs_failed += 1;
2307
- addFailure(stats, "failed_dirs", directory);
2308
- }
2309
- }
2310
- let savedMappingPath;
2311
- if (options.directoryIdsPath === void 0 && options.dryRun !== true) {
2312
- savedMappingPath = options.saveDirectoryIdsPath ?? mappingPath(baseDirectory, options.cacheDirectory);
2313
- await fileSystem.writeMapping(savedMappingPath, directoryIds);
2314
- diagnostics.push(`directory mapping saved: ${savedMappingPath}`);
2315
- }
2316
- if (options.skipUpload !== true) {
2317
- const limit = pLimit(concurrency);
2318
- await Promise.all(tree.files.map((file) => limit(async () => {
2319
- const absolute = join(baseDirectory, file);
2320
- const parentPath = posix(dirname(file)) === "." ? "" : posix(dirname(file));
2321
- const parentId = directoryIds[parentPath] ?? "";
2322
- if (parentPath.length > 0 && parentId.length === 0) {
2323
- stats.files_failed += 1;
2324
- addFailure(stats, "failed_files", file);
2325
- return;
2326
- }
2327
- const title = basename(file);
2328
- const key = qmindFolderSyncIndexKey(parentId, title);
2329
- desiredKeys.add(key);
2330
- const existing = remoteIndex.get(key);
2331
- if (existing?.isDir === true) {
2332
- stats.files_failed += 1;
2333
- addFailure(stats, "failed_files", file);
2334
- return;
2335
- }
2336
- if (existing !== void 0 && !await shouldReplaceQMindFolderFile(mode, existing, absolute, fileSystem)) {
2337
- stats.files_already_exist += 1;
2338
- return;
2339
- }
2340
- if (options.dryRun === true) {
2341
- stats.files_uploaded += 1;
2342
- return;
2343
- }
2344
- try {
2345
- const hash = await fileSystem.sha256(absolute);
2346
- const uploaded = await withUploadRetry(async () => {
2347
- const binary = await host.createFileSource(absolute);
2348
- return await host.client.uploadSource(notebookId, binary, {
2349
- parentSourceId: parentId,
2350
- path: file,
2351
- sha256: hash,
2352
- title
2353
- });
2354
- }, options.sleep ?? defaultSleep);
2355
- if (existing !== void 0) await host.client.deleteSource(notebookId, existing.id);
2356
- remoteIndex.set(key, uploaded);
2357
- remoteById.set(uploaded.id, uploaded);
2358
- stats.files_uploaded += 1;
2359
- } catch {
2360
- stats.files_failed += 1;
2361
- addFailure(stats, "failed_files", file);
2362
- }
2363
- })));
2453
+ function parsePositiveInteger(value, field) {
2454
+ const normalized = optionalTrimmed(value);
2455
+ if (normalized === void 0) return void 0;
2456
+ const parsed = Number(normalized);
2457
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) throw invalidConfig(`${field} must be a positive safe integer`, field);
2458
+ return parsed;
2459
+ }
2460
+ function qmindEnvironmentAssignment(line) {
2461
+ const trimmed = line.trim();
2462
+ if (trimmed.length === 0 || trimmed.startsWith("#")) return void 0;
2463
+ const assignment = trimmed.startsWith("export ") ? trimmed.slice(7) : trimmed;
2464
+ const separator = assignment.indexOf("=");
2465
+ if (separator < 1) return void 0;
2466
+ const key = assignment.slice(0, separator).trim();
2467
+ if (!/^QMIND_[A-Z0-9_]+$/.test(key)) return void 0;
2468
+ let value = assignment.slice(separator + 1).trim();
2469
+ if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
2470
+ return [key, value];
2471
+ }
2472
+ async function mergedEnvironment(environment, legacyEnvironmentPath) {
2473
+ const merged = { ...environment };
2474
+ if (legacyEnvironmentPath === false) return merged;
2475
+ let content;
2476
+ try {
2477
+ content = await readFile(legacyEnvironmentPath, "utf8");
2478
+ } catch (error) {
2479
+ if (error.code === "ENOENT") return merged;
2480
+ throw invalidConfig("unable to read the legacy QMind environment file");
2364
2481
  }
2365
- if (options.dryRun !== true) for (const [directory, id] of Object.entries(directoryIds)) {
2366
- if (id.startsWith("dry-run:")) continue;
2367
- try {
2368
- await host.client.updateSource(notebookId, id, {
2369
- isDir: true,
2370
- title: basename(directory)
2371
- });
2372
- } catch {
2373
- stats.dirs_refresh_failed += 1;
2374
- }
2482
+ for (const line of content.split(/\r?\n/)) {
2483
+ const parsed = qmindEnvironmentAssignment(line);
2484
+ if (parsed === void 0) continue;
2485
+ const [key, value] = parsed;
2486
+ if (merged[key] === void 0) merged[key] = value;
2375
2487
  }
2376
- const preDeleteFailures = stats.files_failed + stats.dirs_failed;
2377
- if (options.delete === true && preDeleteFailures === 0) {
2378
- const deletions = remote.filter((source) => !desiredKeys.has(qmindFolderSyncIndexKey(source.parentSourceId, source.title))).sort((left, right) => {
2379
- if (left.isDir !== right.isDir) return left.isDir ? 1 : -1;
2380
- return right.path.split("/").length - left.path.split("/").length;
2381
- });
2382
- for (const source of deletions) try {
2383
- if (options.dryRun !== true) await host.client.deleteSource(notebookId, source.id);
2384
- if (source.isDir) stats.dirs_deleted = (stats.dirs_deleted ?? 0) + 1;
2385
- else stats.files_deleted = (stats.files_deleted ?? 0) + 1;
2386
- } catch {
2387
- stats.delete_failed = (stats.delete_failed ?? 0) + 1;
2388
- (stats.failed_deletes ??= []).push(source.path || source.title);
2389
- }
2488
+ return merged;
2489
+ }
2490
+ async function fileConfiguration(homeDirectory) {
2491
+ try {
2492
+ return fileConfigSchema.parse(JSON.parse(await readFile(join(homeDirectory, "config.json"), "utf8")));
2493
+ } catch (error) {
2494
+ if (error.code === "ENOENT") return {};
2495
+ throw invalidConfig("stored QMind configuration is invalid");
2390
2496
  }
2391
- const exitCode = stats.files_failed > 0 || stats.dirs_failed > 0 || stats.dirs_refresh_failed > 0 || (stats.delete_failed ?? 0) > 0 ? 1 : 0;
2497
+ }
2498
+ function defaultProcessEnvironment() {
2499
+ return process.env;
2500
+ }
2501
+ async function loadQMindCliConfig(options = {}) {
2502
+ const initialEnvironment = options.environment ?? defaultProcessEnvironment();
2503
+ const baseHome = options.homeDirectory ?? homedir();
2504
+ const environment = await mergedEnvironment(initialEnvironment, options.legacyEnvironmentPath === void 0 ? join(baseHome, ".qmind-env") : options.legacyEnvironmentPath);
2505
+ const configuredHome = optionalTrimmed(environment.QMIND_HOME);
2506
+ const homeDirectory = resolve(options.homeDirectory ?? configuredHome ?? join(baseHome, ".qmind"));
2507
+ const file = await fileConfiguration(homeDirectory);
2508
+ const flags = options.flags ?? {};
2509
+ const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY === true;
2510
+ const presetName = optionalTrimmed(environment.QMIND_ENV) ?? "prod";
2511
+ const preset = PRESETS[presetName] ?? PRESETS.prod;
2512
+ if (!(presetName in PRESETS)) options.reporter?.warn(`warning: unknown QMIND_ENV ${JSON.stringify(presetName)}, falling back to prod`);
2513
+ const sashOrigin = normalizeOrigin$1(optionalTrimmed(flags.sash) ?? optionalTrimmed(environment.QMIND_SASH_URL) ?? optionalTrimmed(file.sash_url) ?? optionalTrimmed(options.credentialSashOrigin) ?? preset.sashOrigin, "sash");
2514
+ const dashboardOrigin = normalizeOrigin$1(optionalTrimmed(flags.dashboard) ?? optionalTrimmed(environment.QMIND_DASHBOARD_URL) ?? optionalTrimmed(file.dashboard_url) ?? preset.dashboardOrigin, "dashboard");
2515
+ const runtime = Object.freeze({
2516
+ client: optionalTrimmed(flags.client) ?? optionalTrimmed(environment.QMIND_CLIENT) ?? optionalTrimmed(file.client) ?? "qmind",
2517
+ dashboardOrigin,
2518
+ homeDirectory,
2519
+ nonInteractive: flags.nonInteractive === true || parseBoolean(environment.QMIND_NON_INTERACTIVE, "QMIND_NON_INTERACTIVE") === true || !stdinIsTTY,
2520
+ sashOrigin,
2521
+ updatePublicKey: optionalTrimmed(environment.QMIND_UPDATE_PUBLIC_KEY) ?? optionalTrimmed(file.update_public_key) ?? ""
2522
+ });
2392
2523
  return {
2393
- diagnostics: Object.freeze(diagnostics),
2394
- exitCode,
2395
- ...savedMappingPath === void 0 ? {} : { mappingPath: savedMappingPath },
2396
- stats: publicStats(stats)
2524
+ debug: parseBoolean(environment.QMIND_DEBUG, "QMIND_DEBUG") ?? false,
2525
+ environment,
2526
+ proxyFromEnvironment: parseBoolean(environment.QMIND_PROXY_FROM_ENV, "QMIND_PROXY_FROM_ENV") ?? file.proxy_from_env ?? true,
2527
+ requestTimeoutMs: parsePositiveInteger(environment.QMIND_REQUEST_TIMEOUT_MS, "QMIND_REQUEST_TIMEOUT_MS") ?? file.request_timeout_ms ?? 3e4,
2528
+ runtime,
2529
+ streamIdleTimeoutMs: parsePositiveInteger(environment.QMIND_STREAM_IDLE_TIMEOUT_MS, "QMIND_STREAM_IDLE_TIMEOUT_MS") ?? file.stream_idle_timeout_ms ?? 12e4,
2530
+ ...(() => {
2531
+ const tlsCaFile = optionalTrimmed(environment.QMIND_TLS_CA_FILE) ?? optionalTrimmed(file.tls_ca_file);
2532
+ return tlsCaFile === void 0 ? {} : { tlsCaFile };
2533
+ })(),
2534
+ tlsRejectUnauthorized: parseBoolean(environment.QMIND_TLS_REJECT_UNAUTHORIZED, "QMIND_TLS_REJECT_UNAUTHORIZED") ?? file.tls_reject_unauthorized ?? true,
2535
+ token: optionalTrimmed(flags.token) ?? optionalTrimmed(environment.QMIND_TOKEN) ?? ""
2397
2536
  };
2398
2537
  }
2399
2538
  //#endregion
2400
- //#region src/auth.ts
2401
- const responseRecordSchema = z.record(z.string(), z.unknown());
2402
- const personalTokenExchangeSchema = z.object({ token: z.string().min(1) });
2403
- const devicePollSchema = z.object({
2539
+ //#region ../node-host/src/credentials.ts
2540
+ const serializedCredentialsSchema = z.object({
2541
+ device_token: z.string().default(""),
2404
2542
  expires_at: z.string().default(""),
2405
2543
  refresh_token: z.string().default(""),
2406
2544
  refresh_token_expires_at: z.string().default(""),
2407
- token: z.string().min(1)
2408
- });
2409
- const refreshSchema = z.object({
2410
- device_token: z.string().min(1),
2411
- expires_at: z.string().default(""),
2412
- refresh_token: z.string().optional(),
2413
- refresh_token_expires_at: z.string().optional()
2414
- });
2415
- const POLL_INTERVAL_MS = 2e3;
2416
- const LOGIN_TIMEOUT_MS = 3e5;
2417
- const EXPIRY_SKEW_MS = 3e4;
2418
- function base64Url(bytes) {
2419
- return Buffer.from(bytes).toString("base64url");
2420
- }
2421
- function createPkce() {
2422
- const verifier = base64Url(randomBytes(32));
2423
- return {
2424
- challenge: createHash("sha256").update(verifier).digest("base64url"),
2425
- nonce: base64Url(randomBytes(16)),
2426
- verifier
2427
- };
2428
- }
2429
- function errorCodeForStatus(status) {
2430
- if (status === 401 || status === 403) return "AUTH_REQUIRED";
2431
- if (status === 404) return "NOT_FOUND";
2432
- if (status === 409) return "CONFLICT";
2433
- if (status === 429) return "RATE_LIMITED";
2434
- if (status >= 400 && status < 500) return "INVALID_ARGUMENT";
2435
- return "REMOTE_ERROR";
2436
- }
2437
- function record$1(value) {
2438
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
2439
- }
2440
- function responseString(response, ...keys) {
2441
- const body = record$1(response.body);
2442
- const nestedError = record$1(body?.error);
2443
- for (const source of [body, nestedError]) for (const key of keys) {
2444
- const value = source?.[key];
2445
- if (typeof value === "string" && value.length > 0) return value;
2446
- }
2545
+ sash_url: z.string().default(""),
2546
+ saved_at: z.string().default(""),
2547
+ schema_version: z.number().int().positive().optional()
2548
+ }).passthrough();
2549
+ function storedCredentialsInvalid() {
2550
+ return new QMindError("AUTH_REQUIRED", "stored QMind credentials are invalid");
2447
2551
  }
2448
- function header(response, name) {
2449
- const normalized = name.toLowerCase();
2450
- for (const [key, value] of Object.entries(response.headers)) if (key.toLowerCase() === normalized && value.length > 0) return value;
2552
+ function isMissing(error) {
2553
+ return error.code === "ENOENT";
2451
2554
  }
2452
- function authResponseError(response, operation) {
2453
- const requestId = responseString(response, "requestId", "request_id") ?? header(response, "x-request-id");
2454
- const upstreamCode = responseString(response, "errorCode", "error_code", "code");
2455
- return new QMindError(errorCodeForStatus(response.status), `${operation} failed`, {
2456
- details: { operation },
2457
- ...requestId === void 0 ? {} : { requestId },
2458
- status: response.status,
2459
- ...upstreamCode === void 0 ? {} : { upstreamCode }
2460
- });
2555
+ function isValidOptionalTimestamp(value) {
2556
+ return value.length === 0 || Number.isFinite(Date.parse(value));
2461
2557
  }
2462
- async function authRequest(transport, method, path, operation, body) {
2463
- const request = {
2464
- ...body === void 0 ? {} : { body: {
2465
- kind: "json",
2466
- value: body
2467
- } },
2468
- destination: {
2469
- kind: "host",
2470
- path
2471
- },
2472
- idempotent: method === "GET",
2473
- method
2474
- };
2475
- let response;
2558
+ function normalizeStoredCredentials(value) {
2559
+ let parsed;
2476
2560
  try {
2477
- response = await transport.request(request);
2478
- } catch (cause) {
2479
- if (cause instanceof QMindError) throw cause;
2480
- throw new QMindError("NETWORK_ERROR", `${operation} failed`);
2561
+ parsed = serializedCredentialsSchema.parse(value);
2562
+ } catch {
2563
+ throw storedCredentialsInvalid();
2481
2564
  }
2482
- if (response.status < 200 || response.status >= 300) throw authResponseError(response, operation);
2483
- const parsed = responseRecordSchema.safeParse(response.body);
2484
- if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", `${operation} returned an invalid response`, {
2485
- details: { operation },
2486
- status: response.status
2487
- });
2488
- return parsed.data;
2489
- }
2490
- function credentialsFromLogin(value, sashOrigin) {
2565
+ if (parsed.schema_version !== void 0 && parsed.schema_version > 2 || parsed.device_token.length === 0 && parsed.refresh_token.length === 0 || !isValidOptionalTimestamp(parsed.expires_at) || !isValidOptionalTimestamp(parsed.refresh_token_expires_at)) throw storedCredentialsInvalid();
2491
2566
  return {
2492
- deviceToken: value.token,
2493
- expiresAt: value.expires_at,
2494
- refreshToken: value.refresh_token,
2495
- refreshTokenExpiresAt: value.refresh_token_expires_at,
2496
- sashOrigin,
2497
- savedAt: "",
2498
- schemaVersion: 2
2567
+ deviceToken: parsed.device_token,
2568
+ expiresAt: parsed.expires_at,
2569
+ refreshToken: parsed.refresh_token,
2570
+ refreshTokenExpiresAt: parsed.refresh_token_expires_at,
2571
+ sashOrigin: parsed.sash_url,
2572
+ savedAt: parsed.saved_at,
2573
+ schemaVersion: parsed.schema_version ?? 1
2499
2574
  };
2500
2575
  }
2501
- function isExpired(timestamp, now, skew = 0) {
2502
- if (timestamp.length === 0) return false;
2503
- const value = Date.parse(timestamp);
2504
- return Number.isFinite(value) && value <= now + skew;
2576
+ function serializedCredentials(credentials, now) {
2577
+ return `${JSON.stringify({
2578
+ device_token: credentials.deviceToken,
2579
+ expires_at: credentials.expiresAt,
2580
+ refresh_token: credentials.refreshToken,
2581
+ refresh_token_expires_at: credentials.refreshTokenExpiresAt,
2582
+ sash_url: credentials.sashOrigin,
2583
+ saved_at: new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z"),
2584
+ schema_version: 2
2585
+ }, null, 2)}\n`;
2505
2586
  }
2506
- function aborted(signal) {
2507
- return signal?.aborted === true ? new QMindError("ABORTED", "QMind authorization was aborted") : void 0;
2587
+ function sameCredentials(left, right) {
2588
+ return left !== void 0 && right !== void 0 && left.deviceToken === right.deviceToken && left.expiresAt === right.expiresAt && left.refreshToken === right.refreshToken && left.refreshTokenExpiresAt === right.refreshTokenExpiresAt && left.sashOrigin === right.sashOrigin && left.schemaVersion === right.schemaVersion;
2508
2589
  }
2509
- function createSystemClock() {
2510
- return Object.freeze({
2511
- now: Date.now,
2512
- sleep(milliseconds, signal) {
2513
- return new Promise((resolve, reject) => {
2514
- const alreadyAborted = aborted(signal);
2515
- if (alreadyAborted !== void 0) {
2516
- reject(alreadyAborted);
2517
- return;
2518
- }
2519
- const onAbort = () => {
2520
- clearTimeout(timer);
2521
- reject(new QMindError("ABORTED", "QMind authorization was aborted"));
2522
- };
2523
- const timer = setTimeout(() => {
2524
- signal?.removeEventListener("abort", onAbort);
2525
- resolve();
2526
- }, milliseconds);
2527
- signal?.addEventListener("abort", onAbort, { once: true });
2528
- });
2529
- }
2590
+ async function secureDirectory(path) {
2591
+ await mkdir(path, {
2592
+ mode: 448,
2593
+ recursive: true
2530
2594
  });
2595
+ const info = await lstat(path);
2596
+ if (!info.isDirectory() || info.isSymbolicLink()) throw storedCredentialsInvalid();
2597
+ if (process.platform !== "win32") await chmod(path, 448);
2531
2598
  }
2532
- function createQMindCliAuthManager(options) {
2533
- let credentials = options.credentials;
2534
- let explicitToken = options.explicitToken;
2535
- let token = "";
2536
- let refreshPromise;
2537
- const login = async (loginOptions = {}) => {
2538
- if (options.config.nonInteractive) throw new QMindError("AUTH_REQUIRED", "login requires an interactive terminal");
2539
- const pkce = createPkce();
2540
- const browserUrl = new URL("/device/selectAccounts", options.config.dashboardOrigin);
2541
- browserUrl.searchParams.set("nonce", pkce.nonce);
2542
- browserUrl.searchParams.set("challenge", pkce.challenge);
2543
- browserUrl.searchParams.set("challenge_method", "S256");
2544
- browserUrl.searchParams.set("machine_id", pkce.nonce);
2545
- if (loginOptions.clientId !== void 0 && loginOptions.clientId.trim().length > 0) browserUrl.searchParams.set("client_id", loginOptions.clientId.trim());
2546
- options.reporter.info(`Opening browser for authorization. If it does not open, visit:\n${browserUrl.toString()}`);
2547
- if (loginOptions.openBrowser !== false) try {
2548
- await options.browser.open(browserUrl.toString());
2549
- } catch {
2550
- options.reporter.warn("warning: unable to open a browser; use the authorization URL above");
2551
- }
2552
- options.reporter.info("Waiting for authorization (timeout: 5m)...");
2553
- const deadline = options.clock.now() + LOGIN_TIMEOUT_MS;
2554
- while (options.clock.now() < deadline) {
2555
- const abortError = aborted(loginOptions.signal);
2556
- if (abortError !== void 0) throw abortError;
2557
- await options.clock.sleep(POLL_INTERVAL_MS, loginOptions.signal);
2558
- const pollPath = `/api/v1/deviceToken/poll?${new URLSearchParams({
2559
- challenge_method: "S256",
2560
- nonce: pkce.nonce,
2561
- verifier: pkce.verifier
2562
- }).toString()}`;
2563
- try {
2564
- const raw = await authRequest(options.transport, "GET", pollPath, "device authorization polling");
2565
- const parsed = devicePollSchema.safeParse(raw);
2566
- if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "device authorization returned an invalid response");
2567
- credentials = credentialsFromLogin(parsed.data, options.config.sashOrigin);
2568
- await options.vault.save(credentials);
2569
- explicitToken = "";
2570
- token = credentials.deviceToken;
2571
- return {
2572
- expiresAt: credentials.expiresAt,
2573
- sashOrigin: credentials.sashOrigin
2574
- };
2575
- } catch (error) {
2576
- if (error instanceof QMindError && error.status === 404) continue;
2577
- throw error;
2578
- }
2599
+ async function secureRegularFile(path) {
2600
+ const info = await lstat(path);
2601
+ if (!info.isFile() || info.isSymbolicLink()) throw storedCredentialsInvalid();
2602
+ if (process.platform !== "win32" && (info.mode & 63) !== 0) await chmod(path, 384);
2603
+ }
2604
+ async function replaceFileAtomically(tempPath, targetPath) {
2605
+ try {
2606
+ await rename(tempPath, targetPath);
2607
+ return;
2608
+ } catch (error) {
2609
+ if (process.platform !== "win32") throw error;
2610
+ }
2611
+ const backupPath = `${targetPath}.${randomBytes(6).toString("hex")}.old`;
2612
+ let backedUp = false;
2613
+ try {
2614
+ try {
2615
+ await rename(targetPath, backupPath);
2616
+ backedUp = true;
2617
+ } catch (error) {
2618
+ if (!isMissing(error)) throw error;
2579
2619
  }
2580
- throw new QMindError("AUTH_REQUIRED", "authorization timed out after 5m");
2620
+ await rename(tempPath, targetPath);
2621
+ if (backedUp) await rm(backupPath, { force: true });
2622
+ } catch (error) {
2623
+ if (backedUp) await rename(backupPath, targetPath).catch(() => void 0);
2624
+ throw error;
2625
+ }
2626
+ }
2627
+ async function optionalSecret(operation, fallback) {
2628
+ try {
2629
+ return await operation();
2630
+ } catch {
2631
+ return fallback;
2632
+ }
2633
+ }
2634
+ function createOptionalKeyringSecretStore() {
2635
+ const entry = async () => {
2636
+ const { Entry } = await import("@napi-rs/keyring");
2637
+ return new Entry("qmind-cli", "device-token");
2581
2638
  };
2582
- const refreshInternal = async (reason) => {
2583
- if (credentials === void 0 || credentials.refreshToken.length === 0) {
2584
- token = "";
2585
- throw new QMindError("AUTH_REQUIRED", `credentials cannot be refreshed after ${reason}`);
2639
+ return Object.freeze({
2640
+ async delete() {
2641
+ await optionalSecret(async () => void (await entry()).deletePassword(), void 0);
2642
+ },
2643
+ async get() {
2644
+ return await optionalSecret(async () => (await entry()).getPassword() ?? void 0, void 0);
2645
+ },
2646
+ async set(token) {
2647
+ await optionalSecret(async () => void (await entry()).setPassword(token), void 0);
2586
2648
  }
2587
- if (isExpired(credentials.refreshTokenExpiresAt, options.clock.now())) {
2588
- await options.vault.delete();
2589
- credentials = void 0;
2590
- token = "";
2591
- throw new QMindError("AUTH_REQUIRED", "stored refresh credentials have expired");
2649
+ });
2650
+ }
2651
+ function createFileCredentialVault(options) {
2652
+ const credentialsPath = join(options.homeDirectory, "credentials.json");
2653
+ const legacyBackupPath = `${credentialsPath}.v1.bak`;
2654
+ const secretStore = options.secretStore ?? createOptionalKeyringSecretStore();
2655
+ const now = options.now ?? Date.now;
2656
+ const loadUnlocked = async () => {
2657
+ let serialized;
2658
+ try {
2659
+ await secureRegularFile(credentialsPath);
2660
+ serialized = JSON.parse(await readFile(credentialsPath, "utf8"));
2661
+ } catch (error) {
2662
+ if (isMissing(error)) return void 0;
2663
+ if (error instanceof QMindError) throw error;
2664
+ throw storedCredentialsInvalid();
2592
2665
  }
2593
- const raw = await authRequest(options.transport, "POST", "/api/v1/deviceToken/refresh", "credential refresh", { refresh_token: credentials.refreshToken });
2594
- const parsed = refreshSchema.safeParse(raw);
2595
- if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "credential refresh returned an invalid response");
2596
- const legacy = credentials.schemaVersion !== 2;
2597
- const refreshed = {
2666
+ const credentials = normalizeStoredCredentials(serialized);
2667
+ const keychainToken = await optionalSecret(() => secretStore.get(), void 0);
2668
+ return keychainToken === void 0 || keychainToken.length === 0 ? credentials : {
2598
2669
  ...credentials,
2599
- deviceToken: parsed.data.device_token,
2600
- expiresAt: parsed.data.expires_at,
2601
- refreshToken: parsed.data.refresh_token ?? credentials.refreshToken,
2602
- refreshTokenExpiresAt: parsed.data.refresh_token_expires_at ?? credentials.refreshTokenExpiresAt,
2603
- sashOrigin: options.config.sashOrigin,
2604
- schemaVersion: 2
2670
+ deviceToken: keychainToken
2605
2671
  };
2606
- await options.vault.save(refreshed, { migrateLegacy: legacy });
2607
- credentials = refreshed;
2608
- token = refreshed.deviceToken;
2609
- return token;
2610
2672
  };
2611
- const manager = {
2612
- async getToken() {
2613
- if (token.length > 0) return token;
2614
- const candidate = explicitToken || credentials?.deviceToken || "";
2615
- if (candidate.length === 0) {
2616
- if (options.config.nonInteractive) throw new QMindError("AUTH_REQUIRED", "no credentials found; run 'qmind login' in an interactive terminal");
2617
- await login();
2618
- return token;
2619
- }
2620
- if (candidate.startsWith("pt-")) {
2621
- const raw = await authRequest(options.transport, "POST", "/api/v1/jobToken/exchange", "personal token exchange", { personal_token: candidate });
2622
- const parsed = personalTokenExchangeSchema.safeParse(raw);
2623
- if (!parsed.success) throw new QMindError("PROTOCOL_ERROR", "personal token exchange returned an invalid response");
2624
- token = parsed.data.token;
2625
- return token;
2673
+ const withLock = async (operation) => {
2674
+ await secureDirectory(options.homeDirectory);
2675
+ const release = await lockfile.lock(options.homeDirectory, {
2676
+ lockfilePath: join(options.homeDirectory, ".credentials.lock"),
2677
+ realpath: false,
2678
+ retries: {
2679
+ factor: 1.5,
2680
+ maxTimeout: 100,
2681
+ minTimeout: 20,
2682
+ retries: 8
2683
+ },
2684
+ stale: 1e4
2685
+ });
2686
+ try {
2687
+ return await operation();
2688
+ } finally {
2689
+ await release();
2690
+ }
2691
+ };
2692
+ const persist = async (credentials, saveOptions = {}) => {
2693
+ const tempPath = `${credentialsPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2694
+ try {
2695
+ if (saveOptions.migrateLegacy === true) try {
2696
+ await secureRegularFile(credentialsPath);
2697
+ await copyFile(credentialsPath, legacyBackupPath);
2698
+ if (process.platform !== "win32") await chmod(legacyBackupPath, 384);
2699
+ } catch (error) {
2700
+ if (!isMissing(error)) throw error;
2626
2701
  }
2627
- if (credentials !== void 0 && candidate === credentials.deviceToken && isExpired(credentials.expiresAt, options.clock.now(), EXPIRY_SKEW_MS)) return manager.refreshToken("expired", candidate);
2628
- token = candidate;
2629
- return token;
2630
- },
2631
- login,
2632
- async logout() {
2633
- await options.vault.delete();
2634
- credentials = void 0;
2635
- explicitToken = "";
2636
- token = "";
2637
- refreshPromise = void 0;
2638
- },
2639
- async refreshToken(reason, rejectedToken) {
2640
- if (rejectedToken !== void 0 && token.length > 0 && token !== rejectedToken) return token;
2641
- refreshPromise ??= refreshInternal(reason).finally(() => {
2642
- refreshPromise = void 0;
2702
+ await writeFile(tempPath, serializedCredentials(credentials, now()), {
2703
+ flag: "wx",
2704
+ mode: 384
2643
2705
  });
2644
- return await refreshPromise;
2706
+ if (process.platform !== "win32") await chmod(tempPath, 384);
2707
+ await replaceFileAtomically(tempPath, credentialsPath);
2708
+ if (process.platform !== "win32") await chmod(credentialsPath, 384);
2709
+ await optionalSecret(() => secretStore.set(credentials.deviceToken), void 0);
2710
+ } finally {
2711
+ await rm(tempPath, { force: true });
2645
2712
  }
2646
2713
  };
2647
- return Object.freeze(manager);
2648
- }
2649
- //#endregion
2650
- //#region src/authenticated-transport.ts
2651
- function withBearer(request, token) {
2652
- return {
2653
- ...request,
2654
- headers: {
2655
- ...request.headers,
2656
- authorization: `Bearer ${token}`
2657
- }
2714
+ const deleteUnlocked = async () => {
2715
+ await Promise.all([rm(credentialsPath, { force: true }), rm(legacyBackupPath, { force: true })]);
2716
+ await optionalSecret(() => secretStore.delete(), void 0);
2658
2717
  };
2659
- }
2660
- function createAuthenticatedQMindTransport(transport, auth) {
2661
2718
  return Object.freeze({
2662
- async request(request) {
2663
- if (request.destination.kind === "signed") return await transport.request(request);
2664
- const token = await auth.getToken();
2665
- const response = await transport.request(withBearer(request, token));
2666
- if (response.status !== 401) return response;
2667
- const refreshed = await auth.refreshToken("unauthorized", token);
2668
- return await transport.request(withBearer(request, refreshed));
2719
+ async compareAndSwap(expected, credentials, saveOptions = {}) {
2720
+ return await withLock(async () => {
2721
+ if (!sameCredentials(await loadUnlocked(), expected)) return false;
2722
+ await persist(credentials, saveOptions);
2723
+ return true;
2724
+ });
2669
2725
  },
2670
- async stream(request) {
2671
- const token = await auth.getToken();
2672
- const response = await transport.stream(withBearer(request, token));
2673
- if (response.kind !== "response" || response.status !== 401) return response;
2674
- const refreshed = await auth.refreshToken("unauthorized", token);
2675
- return await transport.stream(withBearer(request, refreshed));
2726
+ async delete() {
2727
+ await withLock(deleteUnlocked);
2728
+ },
2729
+ async deleteIf(expected) {
2730
+ return await withLock(async () => {
2731
+ if (!sameCredentials(await loadUnlocked(), expected)) return false;
2732
+ await deleteUnlocked();
2733
+ return true;
2734
+ });
2735
+ },
2736
+ async load() {
2737
+ return await loadUnlocked();
2738
+ },
2739
+ async save(credentials, saveOptions = {}) {
2740
+ await withLock(async () => persist(credentials, saveOptions));
2676
2741
  }
2677
2742
  });
2678
2743
  }
2679
2744
  //#endregion
2680
- //#region src/config.ts
2681
- const fileConfigSchema = z.object({
2682
- client: z.string().optional(),
2683
- dashboard_url: z.string().optional(),
2684
- proxy_from_env: z.boolean().optional(),
2685
- request_timeout_ms: z.number().int().positive().optional(),
2686
- sash_url: z.string().optional(),
2687
- stream_idle_timeout_ms: z.number().int().positive().optional(),
2688
- tls_ca_file: z.string().optional(),
2689
- tls_reject_unauthorized: z.boolean().optional(),
2690
- update_public_key: z.string().optional(),
2691
- user_id: z.string().optional()
2692
- }).passthrough();
2693
- const PRESETS = Object.freeze({
2694
- daily: {
2695
- dashboardOrigin: "https://daily.qoder.ai",
2696
- sashOrigin: "https://daily-openapi.qoder.sh"
2697
- },
2698
- prod: {
2699
- dashboardOrigin: "https://qoder.com",
2700
- sashOrigin: "https://openapi.qoder.sh"
2701
- },
2702
- test: {
2703
- dashboardOrigin: "https://test.qoder.ai",
2704
- sashOrigin: "https://test-openapi.qoder.sh"
2745
+ //#region ../node-host/src/files.ts
2746
+ function safeQMindDownloadName(title, fallback) {
2747
+ const normalized = [...title.normalize("NFKC")].map((character) => {
2748
+ const codePoint = character.codePointAt(0) ?? 0;
2749
+ return codePoint <= 31 || codePoint === 127 ? "_" : character;
2750
+ }).join("").replace(/[\\/]/g, "_").replace(/^\.+$/, "").trim().slice(0, 240);
2751
+ const safe = basename(normalized);
2752
+ if (safe.length > 0 && safe !== "." && safe !== "..") return safe;
2753
+ return basename(fallback.replace(/[\\/]/g, "_")) || "download";
2754
+ }
2755
+ async function commitQMindTemporaryFile(temporary, path, overwrite) {
2756
+ if (!overwrite) {
2757
+ await link(temporary, path).catch((error) => {
2758
+ if (error.code === "EEXIST") throw new QMindError("CONFLICT", `target already exists: ${path}`);
2759
+ throw error;
2760
+ });
2761
+ await rm(temporary, { force: true });
2762
+ return;
2763
+ }
2764
+ if (process.platform !== "win32") {
2765
+ await rename(temporary, path);
2766
+ return;
2767
+ }
2768
+ const backup = `${path}.${randomBytes(8).toString("hex")}.old`;
2769
+ let backedUp = false;
2770
+ try {
2771
+ try {
2772
+ await rename(path, backup);
2773
+ backedUp = true;
2774
+ } catch (error) {
2775
+ if (error.code !== "ENOENT") throw error;
2776
+ }
2777
+ await rename(temporary, path);
2778
+ if (backedUp) await rm(backup, { force: true });
2779
+ } catch (error) {
2780
+ if (backedUp) await rename(backup, path).catch(() => void 0);
2781
+ throw error;
2782
+ }
2783
+ }
2784
+ /** Atomic write with a no-clobber default. Temporary files are always cleaned. */
2785
+ async function atomicQMindWrite(path, content, overwrite = false) {
2786
+ await mkdir(dirname(path), { recursive: true });
2787
+ const temporary = join(dirname(path), `.${basename(path)}.${randomBytes(8).toString("hex")}.tmp`);
2788
+ try {
2789
+ await writeFile(temporary, content, {
2790
+ flag: "wx",
2791
+ mode: 420
2792
+ });
2793
+ await commitQMindTemporaryFile(temporary, path, overwrite);
2794
+ } catch (error) {
2795
+ await rm(temporary, { force: true });
2796
+ throw error;
2797
+ }
2798
+ }
2799
+ //#endregion
2800
+ //#region ../node-host/src/node-transport.ts
2801
+ const RETRYABLE_STATUS = /* @__PURE__ */ new Set([
2802
+ 408,
2803
+ 429,
2804
+ 500,
2805
+ 502,
2806
+ 503,
2807
+ 504
2808
+ ]);
2809
+ const CREDENTIAL_HEADERS = /* @__PURE__ */ new Set([
2810
+ "authorization",
2811
+ "cookie",
2812
+ "proxy-authorization",
2813
+ "x-csrf-token",
2814
+ "x-xsrf-token"
2815
+ ]);
2816
+ const SSE_MAX_BUFFER_SIZE = 8388608;
2817
+ function invalidArgument(message, field) {
2818
+ return new QMindError("INVALID_ARGUMENT", message, { details: { field } });
2819
+ }
2820
+ function positiveInteger(value, fallback, field) {
2821
+ if (value === void 0) return fallback;
2822
+ if (!Number.isSafeInteger(value) || value <= 0) throw invalidArgument(`${field} must be a positive safe integer`, field);
2823
+ return value;
2824
+ }
2825
+ function nonNegativeInteger(value, fallback, field) {
2826
+ if (value === void 0) return fallback;
2827
+ if (!Number.isSafeInteger(value) || value < 0) throw invalidArgument(`${field} must be a non-negative safe integer`, field);
2828
+ return value;
2829
+ }
2830
+ function normalizeOrigin(value) {
2831
+ let url;
2832
+ try {
2833
+ url = new URL(value);
2834
+ } catch {
2835
+ throw invalidArgument("origin must be a valid HTTP(S) origin", "origin");
2836
+ }
2837
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw invalidArgument("origin must contain only an HTTP(S) scheme and authority", "origin");
2838
+ return url;
2839
+ }
2840
+ function environmentValue(environment, ...keys) {
2841
+ for (const key of keys) {
2842
+ const value = environment[key]?.trim();
2843
+ if (value !== void 0 && value.length > 0) return value;
2844
+ }
2845
+ }
2846
+ function tlsConnectOptions(options, withTls) {
2847
+ const timeout = positiveInteger(options.connectTimeoutMs, 1e4, "connectTimeoutMs");
2848
+ if (!withTls) return { timeout };
2849
+ const ca = options.tls?.ca;
2850
+ return {
2851
+ ...ca === void 0 ? {} : { ca: typeof ca === "string" ? ca : [...ca] },
2852
+ rejectUnauthorized: options.tls?.rejectUnauthorized ?? true,
2853
+ timeout
2854
+ };
2855
+ }
2856
+ function createDispatchers(options) {
2857
+ const environment = options.environment ?? process.env;
2858
+ if (options.proxyFromEnvironment === false) return {
2859
+ host: new Agent({ connect: tlsConnectOptions(options, true) }),
2860
+ signed: new Agent({ connect: tlsConnectOptions(options, true) })
2861
+ };
2862
+ const httpProxy = environmentValue(environment, "HTTP_PROXY", "http_proxy");
2863
+ const httpsProxy = environmentValue(environment, "HTTPS_PROXY", "https_proxy");
2864
+ const noProxy = environmentValue(environment, "NO_PROXY", "no_proxy");
2865
+ if (httpProxy === void 0 && httpsProxy === void 0) return {
2866
+ host: new Agent({ connect: tlsConnectOptions(options, true) }),
2867
+ signed: new Agent({ connect: tlsConnectOptions(options, true) })
2868
+ };
2869
+ const proxy = {
2870
+ httpProxy: httpProxy ?? "",
2871
+ httpsProxy: httpsProxy ?? "",
2872
+ noProxy: noProxy ?? ""
2873
+ };
2874
+ return {
2875
+ host: new EnvHttpProxyAgent({
2876
+ ...proxy,
2877
+ requestTls: tlsConnectOptions(options, true)
2878
+ }),
2879
+ signed: new EnvHttpProxyAgent({
2880
+ ...proxy,
2881
+ requestTls: tlsConnectOptions(options, true)
2882
+ })
2883
+ };
2884
+ }
2885
+ function headersToRecord(headers) {
2886
+ const result = {};
2887
+ for (const [name, value] of Object.entries(headers)) {
2888
+ if (value === void 0) continue;
2889
+ result[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
2705
2890
  }
2706
- });
2707
- function invalidConfig(message, field) {
2708
- return new QMindError("INVALID_ARGUMENT", message, { ...field === void 0 ? {} : { details: { field } } });
2891
+ return result;
2709
2892
  }
2710
- function normalizeOrigin$1(rawValue, field) {
2711
- let url;
2893
+ function parseRawBody(rawBody) {
2894
+ if (rawBody.trim().length === 0) return void 0;
2712
2895
  try {
2713
- url = new URL(rawValue);
2896
+ return JSON.parse(rawBody);
2714
2897
  } catch {
2715
- throw invalidConfig(`${field} must be a valid HTTP(S) origin`, field);
2898
+ return rawBody;
2716
2899
  }
2717
- if (url.protocol !== "http:" && url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw invalidConfig(`${field} must contain only an HTTP(S) scheme and authority`, field);
2718
- return url.origin;
2719
2900
  }
2720
- function optionalTrimmed(value) {
2721
- const trimmed = value?.trim();
2722
- return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
2901
+ async function wireResponse(response) {
2902
+ const rawBody = await response.body.text();
2903
+ return {
2904
+ body: parseRawBody(rawBody),
2905
+ headers: headersToRecord(response.headers),
2906
+ rawBody,
2907
+ status: response.statusCode
2908
+ };
2723
2909
  }
2724
- function parseBoolean(value, field) {
2725
- const normalized = optionalTrimmed(value)?.toLowerCase();
2726
- if (normalized === void 0) return void 0;
2727
- if (normalized === "1" || normalized === "true") return true;
2728
- if (normalized === "0" || normalized === "false") return false;
2729
- throw invalidConfig(`${field} must be 1, 0, true, or false`, field);
2910
+ function assertWireRequest(request) {
2911
+ if (request.timeoutMs !== void 0) positiveInteger(request.timeoutMs, request.timeoutMs, "timeoutMs");
2912
+ if (request.destination.kind === "host") {
2913
+ if (!request.destination.path.startsWith("/") || request.destination.path.startsWith("//")) throw new QMindError("HOST_POLICY_ERROR", "host destination must be root-relative");
2914
+ return;
2915
+ }
2916
+ let url;
2917
+ try {
2918
+ url = new URL(request.destination.url);
2919
+ } catch {
2920
+ throw new QMindError("HOST_POLICY_ERROR", "signed destination must be valid HTTPS");
2921
+ }
2922
+ if (url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0) throw new QMindError("HOST_POLICY_ERROR", "signed destination must be credential-free HTTPS");
2923
+ for (const name of Object.keys(request.headers ?? {})) if (CREDENTIAL_HEADERS.has(name.toLowerCase())) throw new QMindError("HOST_POLICY_ERROR", "host credentials cannot reach a signed URL", { details: { header: name.toLowerCase() } });
2730
2924
  }
2731
- function parsePositiveInteger(value, field) {
2732
- const normalized = optionalTrimmed(value);
2733
- if (normalized === void 0) return void 0;
2734
- const parsed = Number(normalized);
2735
- if (!Number.isSafeInteger(parsed) || parsed <= 0) throw invalidConfig(`${field} must be a positive safe integer`, field);
2736
- return parsed;
2925
+ function sourceFailure(message, source, yieldedBytes) {
2926
+ return new QMindError("HOST_POLICY_ERROR", message, { details: {
2927
+ filename: source.filename,
2928
+ size: source.size,
2929
+ ...yieldedBytes === void 0 ? {} : { yieldedBytes }
2930
+ } });
2737
2931
  }
2738
- function qmindEnvironmentAssignment(line) {
2739
- const trimmed = line.trim();
2740
- if (trimmed.length === 0 || trimmed.startsWith("#")) return void 0;
2741
- const assignment = trimmed.startsWith("export ") ? trimmed.slice(7) : trimmed;
2742
- const separator = assignment.indexOf("=");
2743
- if (separator < 1) return void 0;
2744
- const key = assignment.slice(0, separator).trim();
2745
- if (!/^QMIND_[A-Z0-9_]+$/.test(key)) return void 0;
2746
- let value = assignment.slice(separator + 1).trim();
2747
- if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
2748
- return [key, value];
2932
+ function assertSource(source) {
2933
+ if (!Number.isSafeInteger(source.size) || source.size <= 0 || typeof source.open !== "function") throw sourceFailure("binary source metadata is invalid", source);
2749
2934
  }
2750
- async function mergedEnvironment(environment, legacyEnvironmentPath) {
2751
- const merged = { ...environment };
2752
- if (legacyEnvironmentPath === false) return merged;
2753
- let content;
2754
- try {
2755
- content = await readFile(legacyEnvironmentPath, "utf8");
2756
- } catch (error) {
2757
- if (error.code === "ENOENT") return merged;
2758
- throw invalidConfig("unable to read the legacy QMind environment file");
2759
- }
2760
- for (const line of content.split(/\r?\n/)) {
2761
- const parsed = qmindEnvironmentAssignment(line);
2762
- if (parsed === void 0) continue;
2763
- const [key, value] = parsed;
2764
- if (merged[key] === void 0) merged[key] = value;
2935
+ async function* sourceBytes(source, signal, onProgress) {
2936
+ assertSource(source);
2937
+ let bytes = 0;
2938
+ for await (const chunk of source.open()) {
2939
+ if (signal?.aborted === true) throw signal.reason;
2940
+ if (!(chunk instanceof Uint8Array)) throw sourceFailure("binary source yielded a non-Uint8Array chunk", source, bytes);
2941
+ bytes += chunk.byteLength;
2942
+ if (bytes > source.size) throw sourceFailure("binary source yielded more bytes than declared", source, bytes);
2943
+ yield chunk;
2944
+ onProgress?.(bytes);
2765
2945
  }
2766
- return merged;
2946
+ if (bytes !== source.size) throw sourceFailure("binary source yielded fewer bytes than declared", source, bytes);
2767
2947
  }
2768
- async function fileConfiguration(homeDirectory) {
2769
- try {
2770
- return fileConfigSchema.parse(JSON.parse(await readFile(join(homeDirectory, "config.json"), "utf8")));
2771
- } catch (error) {
2772
- if (error.code === "ENOENT") return {};
2773
- throw invalidConfig("stored QMind configuration is invalid");
2774
- }
2948
+ function quoteDisposition(value) {
2949
+ return value.replace(/["\r\n]/g, "_");
2775
2950
  }
2776
- function defaultProcessEnvironment() {
2777
- return process.env;
2951
+ function multipartPart(name, value, boundary) {
2952
+ return `--${boundary}\r\nContent-Disposition: form-data; name="${quoteDisposition(name)}"\r\n\r\n${value}\r\n`;
2778
2953
  }
2779
- async function loadQMindCliConfig(options = {}) {
2780
- const initialEnvironment = options.environment ?? defaultProcessEnvironment();
2781
- const baseHome = options.homeDirectory ?? homedir();
2782
- const environment = await mergedEnvironment(initialEnvironment, options.legacyEnvironmentPath === void 0 ? join(baseHome, ".qmind-env") : options.legacyEnvironmentPath);
2783
- const configuredHome = optionalTrimmed(environment.QMIND_HOME);
2784
- const homeDirectory = resolve(options.homeDirectory ?? configuredHome ?? join(baseHome, ".qmind"));
2785
- const file = await fileConfiguration(homeDirectory);
2786
- const flags = options.flags ?? {};
2787
- const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY === true;
2788
- const presetName = optionalTrimmed(environment.QMIND_ENV) ?? "prod";
2789
- const preset = PRESETS[presetName] ?? PRESETS.prod;
2790
- if (!(presetName in PRESETS)) options.reporter?.warn(`warning: unknown QMIND_ENV ${JSON.stringify(presetName)}, falling back to prod`);
2791
- const sashOrigin = normalizeOrigin$1(optionalTrimmed(flags.sash) ?? optionalTrimmed(environment.QMIND_SASH_URL) ?? optionalTrimmed(file.sash_url) ?? optionalTrimmed(options.credentialSashOrigin) ?? preset.sashOrigin, "sash");
2792
- const dashboardOrigin = normalizeOrigin$1(optionalTrimmed(flags.dashboard) ?? optionalTrimmed(environment.QMIND_DASHBOARD_URL) ?? optionalTrimmed(file.dashboard_url) ?? preset.dashboardOrigin, "dashboard");
2793
- const runtime = Object.freeze({
2794
- client: optionalTrimmed(flags.client) ?? optionalTrimmed(environment.QMIND_CLIENT) ?? optionalTrimmed(file.client) ?? "qmind",
2795
- dashboardOrigin,
2796
- homeDirectory,
2797
- nonInteractive: flags.nonInteractive === true || parseBoolean(environment.QMIND_NON_INTERACTIVE, "QMIND_NON_INTERACTIVE") === true || !stdinIsTTY,
2798
- sashOrigin,
2799
- updatePublicKey: optionalTrimmed(environment.QMIND_UPDATE_PUBLIC_KEY) ?? optionalTrimmed(file.update_public_key) ?? "",
2800
- userId: optionalTrimmed(flags.user) ?? optionalTrimmed(environment.QMIND_USER_ID) ?? optionalTrimmed(file.user_id) ?? ""
2801
- });
2954
+ function multipartPreamble(source, boundary) {
2955
+ return `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${quoteDisposition(source.filename)}"\r\nContent-Type: ${source.mimeType}\r\n\r\n`;
2956
+ }
2957
+ function byteLength(value) {
2958
+ return Buffer.byteLength(value, "utf8");
2959
+ }
2960
+ function encodeBody(request, boundary) {
2961
+ const initialHeaders = { ...request.headers };
2962
+ const body = request.body;
2963
+ if (body === void 0) return { headers: initialHeaders };
2964
+ if (body.kind === "json") {
2965
+ initialHeaders["content-type"] ??= "application/json";
2966
+ return {
2967
+ body: JSON.stringify(body.value),
2968
+ headers: initialHeaders
2969
+ };
2970
+ }
2971
+ if (body.kind === "binary") {
2972
+ assertSource(body.source);
2973
+ initialHeaders["content-type"] ??= body.source.mimeType;
2974
+ initialHeaders["content-length"] = String(body.source.size);
2975
+ return {
2976
+ body: Readable.from(sourceBytes(body.source, request.signal, body.onProgress)),
2977
+ headers: initialHeaders
2978
+ };
2979
+ }
2980
+ const multipartBody = body;
2981
+ assertSource(multipartBody.file);
2982
+ const fieldParts = Object.entries(multipartBody.fields).map(([name, value]) => multipartPart(name, value, boundary));
2983
+ const preamble = multipartPreamble(multipartBody.file, boundary);
2984
+ const closing = `\r\n--${boundary}--\r\n`;
2985
+ const contentLength = fieldParts.reduce((total, part) => total + byteLength(part), 0) + byteLength(preamble) + multipartBody.file.size + byteLength(closing);
2986
+ async function* multipart() {
2987
+ for (const part of fieldParts) yield Buffer.from(part);
2988
+ yield Buffer.from(preamble);
2989
+ yield* sourceBytes(multipartBody.file, request.signal, multipartBody.onProgress);
2990
+ yield Buffer.from(closing);
2991
+ }
2992
+ initialHeaders["content-type"] = `multipart/form-data; boundary=${boundary}`;
2993
+ initialHeaders["content-length"] = String(contentLength);
2802
2994
  return {
2803
- debug: parseBoolean(environment.QMIND_DEBUG, "QMIND_DEBUG") ?? false,
2804
- environment,
2805
- proxyFromEnvironment: parseBoolean(environment.QMIND_PROXY_FROM_ENV, "QMIND_PROXY_FROM_ENV") ?? file.proxy_from_env ?? true,
2806
- requestTimeoutMs: parsePositiveInteger(environment.QMIND_REQUEST_TIMEOUT_MS, "QMIND_REQUEST_TIMEOUT_MS") ?? file.request_timeout_ms ?? 3e4,
2807
- runtime,
2808
- streamIdleTimeoutMs: parsePositiveInteger(environment.QMIND_STREAM_IDLE_TIMEOUT_MS, "QMIND_STREAM_IDLE_TIMEOUT_MS") ?? file.stream_idle_timeout_ms ?? 12e4,
2809
- ...(() => {
2810
- const tlsCaFile = optionalTrimmed(environment.QMIND_TLS_CA_FILE) ?? optionalTrimmed(file.tls_ca_file);
2811
- return tlsCaFile === void 0 ? {} : { tlsCaFile };
2812
- })(),
2813
- tlsRejectUnauthorized: parseBoolean(environment.QMIND_TLS_REJECT_UNAUTHORIZED, "QMIND_TLS_REJECT_UNAUTHORIZED") ?? file.tls_reject_unauthorized ?? true,
2814
- token: optionalTrimmed(flags.token) ?? optionalTrimmed(environment.QMIND_TOKEN) ?? ""
2995
+ body: Readable.from(multipart()),
2996
+ headers: initialHeaders
2815
2997
  };
2816
2998
  }
2817
- //#endregion
2818
- //#region src/credentials.ts
2819
- const serializedCredentialsSchema = z.object({
2820
- device_token: z.string().default(""),
2821
- expires_at: z.string().default(""),
2822
- refresh_token: z.string().default(""),
2823
- refresh_token_expires_at: z.string().default(""),
2824
- sash_url: z.string().default(""),
2825
- saved_at: z.string().default(""),
2826
- schema_version: z.number().int().positive().optional()
2827
- }).passthrough();
2828
- function storedCredentialsInvalid() {
2829
- return new QMindError("AUTH_REQUIRED", "stored QMind credentials are invalid");
2999
+ function isTimeoutCause(cause) {
3000
+ const code = cause.code;
3001
+ const name = cause.name;
3002
+ return code === "QMIND_REQUEST_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || name === "TimeoutError";
2830
3003
  }
2831
- function isMissing(error) {
2832
- return error.code === "ENOENT";
3004
+ function safeCause(cause) {
3005
+ const record = cause;
3006
+ return {
3007
+ ...typeof record.name === "string" ? { name: record.name } : {},
3008
+ ...typeof record.code === "string" ? { code: record.code } : {}
3009
+ };
2833
3010
  }
2834
- function isValidOptionalTimestamp(value) {
2835
- return value.length === 0 || Number.isFinite(Date.parse(value));
3011
+ function transportFailure(request, cause, deadlineExpired = false) {
3012
+ if (cause instanceof QMindError) return cause;
3013
+ if (request.signal?.aborted === true) return new QMindError("ABORTED", "QMind request was aborted");
3014
+ if (deadlineExpired || isTimeoutCause(cause)) return new QMindError("TIMEOUT", "QMind request timed out", { cause: safeCause(cause) });
3015
+ return new QMindError("NETWORK_ERROR", "QMind network request failed", { cause: safeCause(cause) });
2836
3016
  }
2837
- function normalizeStoredCredentials(value) {
2838
- let parsed;
2839
- try {
2840
- parsed = serializedCredentialsSchema.parse(value);
2841
- } catch {
2842
- throw storedCredentialsInvalid();
2843
- }
2844
- if (parsed.schema_version !== void 0 && parsed.schema_version > 2 || parsed.device_token.length === 0 && parsed.refresh_token.length === 0 || !isValidOptionalTimestamp(parsed.expires_at) || !isValidOptionalTimestamp(parsed.refresh_token_expires_at)) throw storedCredentialsInvalid();
3017
+ function requestDeadline(signal, milliseconds) {
3018
+ const controller = new AbortController();
3019
+ let timedOut = false;
3020
+ const timeout = setTimeout(() => {
3021
+ timedOut = true;
3022
+ const error = Object.assign(/* @__PURE__ */ new Error("QMind request deadline expired"), {
3023
+ code: "QMIND_REQUEST_TIMEOUT",
3024
+ name: "TimeoutError"
3025
+ });
3026
+ controller.abort(error);
3027
+ }, milliseconds);
3028
+ timeout.unref();
3029
+ const onAbort = () => controller.abort(signal?.reason);
3030
+ if (signal?.aborted === true) onAbort();
3031
+ else signal?.addEventListener("abort", onAbort, { once: true });
3032
+ const clearDeadline = () => clearTimeout(timeout);
2845
3033
  return {
2846
- deviceToken: parsed.device_token,
2847
- expiresAt: parsed.expires_at,
2848
- refreshToken: parsed.refresh_token,
2849
- refreshTokenExpiresAt: parsed.refresh_token_expires_at,
2850
- sashOrigin: parsed.sash_url,
2851
- savedAt: parsed.saved_at,
2852
- schemaVersion: parsed.schema_version ?? 1
3034
+ clearTimeout: clearDeadline,
3035
+ dispose() {
3036
+ clearDeadline();
3037
+ signal?.removeEventListener("abort", onAbort);
3038
+ },
3039
+ signal: controller.signal,
3040
+ get timedOut() {
3041
+ return timedOut;
3042
+ }
2853
3043
  };
2854
3044
  }
2855
- function serializedCredentials(credentials, now) {
2856
- return `${JSON.stringify({
2857
- device_token: credentials.deviceToken,
2858
- expires_at: credentials.expiresAt,
2859
- refresh_token: credentials.refreshToken,
2860
- refresh_token_expires_at: credentials.refreshTokenExpiresAt,
2861
- sash_url: credentials.sashOrigin,
2862
- saved_at: new Date(now).toISOString().replace(/\.\d{3}Z$/, "Z"),
2863
- schema_version: 2
2864
- }, null, 2)}\n`;
2865
- }
2866
- async function secureDirectory(path) {
2867
- await mkdir(path, {
2868
- mode: 448,
2869
- recursive: true
3045
+ function abortableSleep(milliseconds, signal) {
3046
+ return new Promise((resolve, reject) => {
3047
+ if (signal?.aborted === true) {
3048
+ reject(signal.reason);
3049
+ return;
3050
+ }
3051
+ const onAbort = () => {
3052
+ clearTimeout(timer);
3053
+ reject(signal?.reason);
3054
+ };
3055
+ const timer = setTimeout(() => {
3056
+ signal?.removeEventListener("abort", onAbort);
3057
+ resolve();
3058
+ }, milliseconds);
3059
+ signal?.addEventListener("abort", onAbort, { once: true });
2870
3060
  });
2871
- const info = await lstat(path);
2872
- if (!info.isDirectory() || info.isSymbolicLink()) throw storedCredentialsInvalid();
2873
- if (process.platform !== "win32") await chmod(path, 448);
2874
- }
2875
- async function secureRegularFile(path) {
2876
- const info = await lstat(path);
2877
- if (!info.isFile() || info.isSymbolicLink()) throw storedCredentialsInvalid();
2878
- if (process.platform !== "win32" && (info.mode & 63) !== 0) await chmod(path, 384);
2879
3061
  }
2880
- async function replaceFileAtomically(tempPath, targetPath) {
2881
- try {
2882
- await rename(tempPath, targetPath);
2883
- return;
2884
- } catch (error) {
2885
- if (process.platform !== "win32") throw error;
2886
- }
2887
- const backupPath = `${targetPath}.${randomBytes(6).toString("hex")}.old`;
2888
- let backedUp = false;
2889
- try {
2890
- try {
2891
- await rename(targetPath, backupPath);
2892
- backedUp = true;
2893
- } catch (error) {
2894
- if (!isMissing(error)) throw error;
2895
- }
2896
- await rename(tempPath, targetPath);
2897
- if (backedUp) await rm(backupPath, { force: true });
2898
- } catch (error) {
2899
- if (backedUp) await rename(backupPath, targetPath).catch(() => void 0);
2900
- throw error;
2901
- }
3062
+ function requestUrl(origin, request) {
3063
+ return request.destination.kind === "host" ? new URL(request.destination.path, origin) : new URL(request.destination.url);
2902
3064
  }
2903
- async function optionalSecret(operation, fallback) {
2904
- try {
2905
- return await operation();
2906
- } catch {
2907
- return fallback;
2908
- }
3065
+ function debugRequest(options, request, url) {
3066
+ options.debugLog?.(`[qmind-debug] ${request.method} ${url.origin}${url.pathname}`);
2909
3067
  }
2910
- function createOptionalKeyringSecretStore() {
2911
- const entry = async () => {
2912
- const { Entry } = await import("@napi-rs/keyring");
2913
- return new Entry("qmind-cli", "device-token");
2914
- };
2915
- return Object.freeze({
2916
- async delete() {
2917
- await optionalSecret(async () => void (await entry()).deletePassword(), void 0);
2918
- },
2919
- async get() {
2920
- return await optionalSecret(async () => (await entry()).getPassword() ?? void 0, void 0);
3068
+ async function* decodedEvents(body, request, disposeSignal) {
3069
+ const decoder = new TextDecoder("utf-8", { fatal: true });
3070
+ const pending = [];
3071
+ let done = false;
3072
+ let parserFailure;
3073
+ const parser = createParser({
3074
+ maxBufferSize: SSE_MAX_BUFFER_SIZE,
3075
+ onError(error) {
3076
+ if (error.type === "max-buffer-size-exceeded") parserFailure = error;
2921
3077
  },
2922
- async set(token) {
2923
- await optionalSecret(async () => void (await entry()).setPassword(token), void 0);
3078
+ onEvent(event) {
3079
+ if (event.data.trim() === "[DONE]") done = true;
3080
+ else pending.push(event.data);
2924
3081
  }
2925
3082
  });
2926
- }
2927
- function createFileCredentialVault(options) {
2928
- const credentialsPath = join(options.homeDirectory, "credentials.json");
2929
- const legacyBackupPath = `${credentialsPath}.v1.bak`;
2930
- const secretStore = options.secretStore ?? createOptionalKeyringSecretStore();
2931
- const now = options.now ?? Date.now;
2932
- const withLock = async (operation) => {
2933
- await secureDirectory(options.homeDirectory);
2934
- const release = await lockfile.lock(options.homeDirectory, {
2935
- lockfilePath: join(options.homeDirectory, ".credentials.lock"),
2936
- realpath: false,
2937
- retries: {
2938
- factor: 1.5,
2939
- maxTimeout: 100,
2940
- minTimeout: 20,
2941
- retries: 8
2942
- },
2943
- stale: 1e4
2944
- });
2945
- try {
2946
- return await operation();
2947
- } finally {
2948
- await release();
3083
+ const drain = function* () {
3084
+ while (pending.length > 0) {
3085
+ const value = pending.shift();
3086
+ if (value !== void 0) yield value;
2949
3087
  }
2950
3088
  };
3089
+ try {
3090
+ for await (const chunk of body) {
3091
+ parser.feed(decoder.decode(chunk, { stream: true }));
3092
+ if (parserFailure !== void 0) throw new QMindError("STREAM_ERROR", "SSE parser buffer limit exceeded", { details: { limitBytes: SSE_MAX_BUFFER_SIZE } });
3093
+ yield* drain();
3094
+ if (done) return;
3095
+ }
3096
+ parser.feed(decoder.decode());
3097
+ parser.reset({ consume: true });
3098
+ if (parserFailure !== void 0) throw new QMindError("STREAM_ERROR", "SSE parser buffer limit exceeded", { details: { limitBytes: SSE_MAX_BUFFER_SIZE } });
3099
+ yield* drain();
3100
+ } catch (cause) {
3101
+ if (cause instanceof QMindError) throw cause;
3102
+ if (request.signal?.aborted === true) throw new QMindError("ABORTED", "QMind stream was aborted");
3103
+ if (isTimeoutCause(cause)) throw new QMindError("TIMEOUT", "QMind stream timed out", { cause: safeCause(cause) });
3104
+ throw new QMindError("STREAM_ERROR", "QMind SSE stream failed", { cause: safeCause(cause) });
3105
+ } finally {
3106
+ disposeSignal();
3107
+ body.destroy();
3108
+ }
3109
+ }
3110
+ function createQMindNodeTransport(options) {
3111
+ const origin = normalizeOrigin(options.origin);
3112
+ const requestTimeoutMs = positiveInteger(options.requestTimeoutMs, 3e4, "requestTimeoutMs");
3113
+ const streamIdleTimeoutMs = positiveInteger(options.streamIdleTimeoutMs, 12e4, "streamIdleTimeoutMs");
3114
+ const maxRetries = nonNegativeInteger(options.maxRetries, 2, "maxRetries");
3115
+ const dispatchers = createDispatchers(options);
3116
+ const dispatcherFor = (request) => request.destination.kind === "host" ? dispatchers.host : dispatchers.signed;
2951
3117
  return Object.freeze({
2952
- async delete() {
2953
- await withLock(async () => {
2954
- await Promise.all([rm(credentialsPath, { force: true }), rm(legacyBackupPath, { force: true })]);
2955
- await optionalSecret(() => secretStore.delete(), void 0);
2956
- });
3118
+ async close() {
3119
+ const unique = /* @__PURE__ */ new Set([dispatchers.host, dispatchers.signed]);
3120
+ await Promise.all([...unique].map(async (dispatcher) => dispatcher.close()));
3121
+ },
3122
+ async downloadToFile(download) {
3123
+ const request$1 = {
3124
+ destination: {
3125
+ kind: "signed",
3126
+ url: download.url
3127
+ },
3128
+ idempotent: true,
3129
+ method: "GET",
3130
+ ...download.signal === void 0 ? {} : { signal: download.signal }
3131
+ };
3132
+ assertWireRequest(request$1);
3133
+ const url = requestUrl(origin, request$1);
3134
+ await mkdir(dirname(download.targetPath), { recursive: true });
3135
+ if (download.overwrite !== true) try {
3136
+ await stat(download.targetPath);
3137
+ throw new QMindError("CONFLICT", `target already exists: ${download.targetPath}`);
3138
+ } catch (error) {
3139
+ if (error.code !== "ENOENT") throw error;
3140
+ }
3141
+ const temporary = join(dirname(download.targetPath), `.${basename(download.targetPath)}.${randomBytes(8).toString("hex")}.tmp`);
3142
+ let response;
3143
+ try {
3144
+ response = await request(url, {
3145
+ bodyTimeout: requestTimeoutMs,
3146
+ dispatcher: dispatchers.signed,
3147
+ headersTimeout: requestTimeoutMs,
3148
+ idempotent: true,
3149
+ method: "GET",
3150
+ ...download.signal === void 0 ? {} : { signal: download.signal }
3151
+ });
3152
+ if (response.statusCode < 200 || response.statusCode >= 300) {
3153
+ const decoded = await wireResponse(response);
3154
+ throw new QMindError("REMOTE_ERROR", "download request failed", {
3155
+ ...decoded.rawBody === void 0 ? {} : { rawBody: decoded.rawBody },
3156
+ status: decoded.status
3157
+ });
3158
+ }
3159
+ await pipeline(response.body, createWriteStream(temporary, {
3160
+ flags: "wx",
3161
+ mode: 384
3162
+ }));
3163
+ const size = (await stat(temporary)).size;
3164
+ await chmod(temporary, 420);
3165
+ await commitQMindTemporaryFile(temporary, download.targetPath, download.overwrite === true);
3166
+ return size;
3167
+ } catch (cause) {
3168
+ response?.body.destroy();
3169
+ await rm(temporary, { force: true });
3170
+ if (cause instanceof QMindError) throw cause;
3171
+ throw transportFailure(request$1, cause);
3172
+ }
3173
+ },
3174
+ async request(request$2) {
3175
+ assertWireRequest(request$2);
3176
+ const url = requestUrl(origin, request$2);
3177
+ const attempts = request$2.idempotent ? maxRetries + 1 : 1;
3178
+ let lastFailure;
3179
+ let lastDeadlineExpired = false;
3180
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
3181
+ const deadline = requestDeadline(request$2.signal, request$2.timeoutMs ?? requestTimeoutMs);
3182
+ try {
3183
+ debugRequest(options, request$2, url);
3184
+ const encoded = encodeBody(request$2, `----qmind-${randomBytes(18).toString("hex")}`);
3185
+ const response = await request(url, {
3186
+ ...encoded.body === void 0 ? {} : { body: encoded.body },
3187
+ bodyTimeout: request$2.timeoutMs ?? requestTimeoutMs,
3188
+ dispatcher: dispatcherFor(request$2),
3189
+ headers: encoded.headers,
3190
+ headersTimeout: request$2.timeoutMs ?? requestTimeoutMs,
3191
+ idempotent: request$2.idempotent,
3192
+ method: request$2.method,
3193
+ signal: deadline.signal
3194
+ });
3195
+ if (attempt + 1 < attempts && RETRYABLE_STATUS.has(response.statusCode)) {
3196
+ await response.body.dump();
3197
+ await abortableSleep(250 * 2 ** attempt, request$2.signal);
3198
+ continue;
3199
+ }
3200
+ return await wireResponse(response);
3201
+ } catch (cause) {
3202
+ lastFailure = cause;
3203
+ lastDeadlineExpired = deadline.timedOut;
3204
+ if (attempt + 1 < attempts && request$2.signal?.aborted !== true) {
3205
+ await abortableSleep(250 * 2 ** attempt, request$2.signal);
3206
+ continue;
3207
+ }
3208
+ } finally {
3209
+ deadline.dispose();
3210
+ }
3211
+ }
3212
+ throw transportFailure(request$2, lastFailure, lastDeadlineExpired);
2957
3213
  },
2958
- async load() {
2959
- let serialized;
3214
+ async stream(request$3) {
3215
+ assertWireRequest(request$3);
3216
+ const url = requestUrl(origin, request$3);
3217
+ debugRequest(options, request$3, url);
3218
+ const encoded = encodeBody(request$3, `----qmind-${randomBytes(18).toString("hex")}`);
3219
+ let response;
3220
+ const deadline = requestDeadline(request$3.signal, request$3.timeoutMs ?? requestTimeoutMs);
2960
3221
  try {
2961
- await secureRegularFile(credentialsPath);
2962
- serialized = JSON.parse(await readFile(credentialsPath, "utf8"));
2963
- } catch (error) {
2964
- if (isMissing(error)) return void 0;
2965
- if (error instanceof QMindError) throw error;
2966
- throw storedCredentialsInvalid();
3222
+ response = await request(url, {
3223
+ ...encoded.body === void 0 ? {} : { body: encoded.body },
3224
+ bodyTimeout: streamIdleTimeoutMs,
3225
+ dispatcher: dispatchers.host,
3226
+ headers: {
3227
+ accept: "text/event-stream",
3228
+ ...encoded.headers
3229
+ },
3230
+ headersTimeout: request$3.timeoutMs ?? requestTimeoutMs,
3231
+ idempotent: request$3.idempotent,
3232
+ method: request$3.method,
3233
+ signal: deadline.signal
3234
+ });
3235
+ } catch (cause) {
3236
+ deadline.dispose();
3237
+ throw transportFailure(request$3, cause, deadline.timedOut);
3238
+ }
3239
+ deadline.clearTimeout();
3240
+ const headers = headersToRecord(response.headers);
3241
+ if (response.statusCode < 200 || response.statusCode >= 300 || !headers["content-type"]?.toLowerCase().includes("text/event-stream")) try {
3242
+ return {
3243
+ ...await wireResponse(response),
3244
+ kind: "response"
3245
+ };
3246
+ } finally {
3247
+ deadline.dispose();
2967
3248
  }
2968
- const credentials = normalizeStoredCredentials(serialized);
2969
- const keychainToken = await optionalSecret(() => secretStore.get(), void 0);
2970
- return keychainToken === void 0 || keychainToken.length === 0 ? credentials : {
2971
- ...credentials,
2972
- deviceToken: keychainToken
3249
+ return {
3250
+ events: decodedEvents(response.body, request$3, deadline.dispose),
3251
+ headers,
3252
+ kind: "events",
3253
+ status: response.statusCode
2973
3254
  };
2974
- },
2975
- async save(credentials, saveOptions = {}) {
2976
- await withLock(async () => {
2977
- const tempPath = `${credentialsPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
2978
- try {
2979
- if (saveOptions.migrateLegacy === true) try {
2980
- await secureRegularFile(credentialsPath);
2981
- await copyFile(credentialsPath, legacyBackupPath);
2982
- if (process.platform !== "win32") await chmod(legacyBackupPath, 384);
2983
- } catch (error) {
2984
- if (!isMissing(error)) throw error;
2985
- }
2986
- await writeFile(tempPath, serializedCredentials(credentials, now()), {
2987
- flag: "wx",
2988
- mode: 384
2989
- });
2990
- if (process.platform !== "win32") await chmod(tempPath, 384);
2991
- await replaceFileAtomically(tempPath, credentialsPath);
2992
- if (process.platform !== "win32") await chmod(credentialsPath, 384);
2993
- await optionalSecret(() => secretStore.set(credentials.deviceToken), void 0);
2994
- } finally {
2995
- await rm(tempPath, { force: true });
2996
- }
2997
- });
2998
3255
  }
2999
3256
  });
3000
3257
  }
3001
- //#endregion
3002
- //#region src/node-transport.ts
3003
- const RETRYABLE_STATUS = /* @__PURE__ */ new Set([
3004
- 408,
3005
- 429,
3006
- 500,
3007
- 502,
3008
- 503,
3009
- 504
3010
- ]);
3011
- const CREDENTIAL_HEADERS = /* @__PURE__ */ new Set([
3012
- "authorization",
3013
- "cookie",
3014
- "proxy-authorization",
3015
- "x-csrf-token",
3016
- "x-xsrf-token"
3017
- ]);
3018
- const SSE_MAX_BUFFER_SIZE = 8388608;
3019
- function invalidArgument(message, field) {
3020
- return new QMindError("INVALID_ARGUMENT", message, { details: { field } });
3021
- }
3022
- function positiveInteger(value, fallback, field) {
3023
- if (value === void 0) return fallback;
3024
- if (!Number.isSafeInteger(value) || value <= 0) throw invalidArgument(`${field} must be a positive safe integer`, field);
3025
- return value;
3026
- }
3027
- function nonNegativeInteger(value, fallback, field) {
3028
- if (value === void 0) return fallback;
3029
- if (!Number.isSafeInteger(value) || value < 0) throw invalidArgument(`${field} must be a non-negative safe integer`, field);
3030
- return value;
3031
- }
3032
- function normalizeOrigin(value) {
3033
- let url;
3034
- try {
3035
- url = new URL(value);
3036
- } catch {
3037
- throw invalidArgument("origin must be a valid HTTP(S) origin", "origin");
3038
- }
3039
- if (url.protocol !== "http:" && url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0 || url.pathname !== "/" || url.search.length > 0 || url.hash.length > 0) throw invalidArgument("origin must contain only an HTTP(S) scheme and authority", "origin");
3040
- return url;
3041
- }
3042
- function environmentValue(environment, ...keys) {
3043
- for (const key of keys) {
3044
- const value = environment[key]?.trim();
3045
- if (value !== void 0 && value.length > 0) return value;
3046
- }
3047
- }
3048
- function tlsConnectOptions(options, withTls) {
3049
- const timeout = positiveInteger(options.connectTimeoutMs, 1e4, "connectTimeoutMs");
3050
- if (!withTls) return { timeout };
3051
- const ca = options.tls?.ca;
3052
- return {
3053
- ...ca === void 0 ? {} : { ca: typeof ca === "string" ? ca : [...ca] },
3054
- rejectUnauthorized: options.tls?.rejectUnauthorized ?? true,
3055
- timeout
3056
- };
3057
- }
3058
- function createDispatchers(options) {
3059
- const environment = options.environment ?? process.env;
3060
- if (options.proxyFromEnvironment === false) return {
3061
- host: new Agent({ connect: tlsConnectOptions(options, true) }),
3062
- signed: new Agent({ connect: tlsConnectOptions(options, true) })
3063
- };
3064
- const httpProxy = environmentValue(environment, "HTTP_PROXY", "http_proxy");
3065
- const httpsProxy = environmentValue(environment, "HTTPS_PROXY", "https_proxy");
3066
- const noProxy = environmentValue(environment, "NO_PROXY", "no_proxy");
3067
- const proxy = {
3068
- ...httpProxy === void 0 ? {} : { httpProxy },
3069
- ...httpsProxy === void 0 ? {} : { httpsProxy },
3070
- ...noProxy === void 0 ? {} : { noProxy }
3071
- };
3072
- return {
3073
- host: new EnvHttpProxyAgent({
3074
- ...proxy,
3075
- requestTls: tlsConnectOptions(options, true)
3076
- }),
3077
- signed: new EnvHttpProxyAgent({
3078
- ...proxy,
3079
- requestTls: tlsConnectOptions(options, true)
3080
- })
3081
- };
3082
- }
3083
- function headersToRecord(headers) {
3084
- const result = {};
3085
- for (const [name, value] of Object.entries(headers)) {
3086
- if (value === void 0) continue;
3087
- result[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
3088
- }
3089
- return result;
3090
- }
3091
- function parseRawBody(rawBody) {
3092
- if (rawBody.trim().length === 0) return void 0;
3093
- try {
3094
- return JSON.parse(rawBody);
3095
- } catch {
3096
- return rawBody;
3097
- }
3098
- }
3099
- async function wireResponse(response) {
3100
- const rawBody = await response.body.text();
3101
- return {
3102
- body: parseRawBody(rawBody),
3103
- headers: headersToRecord(response.headers),
3104
- rawBody,
3105
- status: response.statusCode
3106
- };
3107
- }
3108
- function assertWireRequest(request) {
3109
- if (request.timeoutMs !== void 0) positiveInteger(request.timeoutMs, request.timeoutMs, "timeoutMs");
3110
- if (request.destination.kind === "host") {
3111
- if (!request.destination.path.startsWith("/") || request.destination.path.startsWith("//")) throw new QMindError("HOST_POLICY_ERROR", "host destination must be root-relative");
3112
- return;
3113
- }
3114
- let url;
3115
- try {
3116
- url = new URL(request.destination.url);
3117
- } catch {
3118
- throw new QMindError("HOST_POLICY_ERROR", "signed destination must be valid HTTPS");
3119
- }
3120
- if (url.protocol !== "https:" || url.username.length > 0 || url.password.length > 0) throw new QMindError("HOST_POLICY_ERROR", "signed destination must be credential-free HTTPS");
3121
- for (const name of Object.keys(request.headers ?? {})) if (CREDENTIAL_HEADERS.has(name.toLowerCase())) throw new QMindError("HOST_POLICY_ERROR", "host credentials cannot reach a signed URL", { details: { header: name.toLowerCase() } });
3122
- }
3123
- function sourceFailure(message, source, yieldedBytes) {
3124
- return new QMindError("HOST_POLICY_ERROR", message, { details: {
3125
- filename: source.filename,
3126
- size: source.size,
3127
- ...yieldedBytes === void 0 ? {} : { yieldedBytes }
3128
- } });
3129
- }
3130
- function assertSource(source) {
3131
- if (!Number.isSafeInteger(source.size) || source.size <= 0 || typeof source.open !== "function") throw sourceFailure("binary source metadata is invalid", source);
3132
- }
3133
- async function* sourceBytes(source, signal, onProgress) {
3134
- assertSource(source);
3135
- let bytes = 0;
3136
- for await (const chunk of source.open()) {
3137
- if (signal?.aborted === true) throw signal.reason;
3138
- if (!(chunk instanceof Uint8Array)) throw sourceFailure("binary source yielded a non-Uint8Array chunk", source, bytes);
3139
- bytes += chunk.byteLength;
3140
- if (bytes > source.size) throw sourceFailure("binary source yielded more bytes than declared", source, bytes);
3141
- yield chunk;
3142
- onProgress?.(bytes);
3143
- }
3144
- if (bytes !== source.size) throw sourceFailure("binary source yielded fewer bytes than declared", source, bytes);
3145
- }
3146
- function quoteDisposition(value) {
3147
- return value.replace(/["\r\n]/g, "_");
3148
- }
3149
- function multipartPart(name, value, boundary) {
3150
- return `--${boundary}\r\nContent-Disposition: form-data; name="${quoteDisposition(name)}"\r\n\r\n${value}\r\n`;
3151
- }
3152
- function multipartPreamble(source, boundary) {
3153
- return `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${quoteDisposition(source.filename)}"\r\nContent-Type: ${source.mimeType}\r\n\r\n`;
3154
- }
3155
- function byteLength(value) {
3156
- return Buffer.byteLength(value, "utf8");
3157
- }
3158
- function encodeBody(request, boundary) {
3159
- const initialHeaders = { ...request.headers };
3160
- const body = request.body;
3161
- if (body === void 0) return { headers: initialHeaders };
3162
- if (body.kind === "json") {
3163
- initialHeaders["content-type"] ??= "application/json";
3164
- return {
3165
- body: JSON.stringify(body.value),
3166
- headers: initialHeaders
3167
- };
3168
- }
3169
- if (body.kind === "binary") {
3170
- assertSource(body.source);
3171
- initialHeaders["content-type"] ??= body.source.mimeType;
3172
- initialHeaders["content-length"] = String(body.source.size);
3173
- return {
3174
- body: Readable.from(sourceBytes(body.source, request.signal, body.onProgress)),
3175
- headers: initialHeaders
3176
- };
3177
- }
3178
- const multipartBody = body;
3179
- assertSource(multipartBody.file);
3180
- const fieldParts = Object.entries(multipartBody.fields).map(([name, value]) => multipartPart(name, value, boundary));
3181
- const preamble = multipartPreamble(multipartBody.file, boundary);
3182
- const closing = `\r\n--${boundary}--\r\n`;
3183
- const contentLength = fieldParts.reduce((total, part) => total + byteLength(part), 0) + byteLength(preamble) + multipartBody.file.size + byteLength(closing);
3184
- async function* multipart() {
3185
- for (const part of fieldParts) yield Buffer.from(part);
3186
- yield Buffer.from(preamble);
3187
- yield* sourceBytes(multipartBody.file, request.signal, multipartBody.onProgress);
3188
- yield Buffer.from(closing);
3258
+ const MIME_TYPES = Object.freeze({
3259
+ ".csv": "text/csv",
3260
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
3261
+ ".gif": "image/gif",
3262
+ ".jpeg": "image/jpeg",
3263
+ ".jpg": "image/jpeg",
3264
+ ".md": "text/markdown",
3265
+ ".mdx": "text/markdown",
3266
+ ".pdf": "application/pdf",
3267
+ ".png": "image/png",
3268
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
3269
+ ".svg": "image/svg+xml",
3270
+ ".txt": "text/plain",
3271
+ ".webp": "image/webp",
3272
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
3273
+ });
3274
+ function detectQMindMimeType(filename) {
3275
+ return MIME_TYPES[extname(filename).toLowerCase()] ?? "application/octet-stream";
3276
+ }
3277
+ async function createQMindFileSource(path, options = {}) {
3278
+ let info;
3279
+ try {
3280
+ info = await stat(path);
3281
+ } catch {
3282
+ throw invalidArgument("source path must reference a readable file", "path");
3189
3283
  }
3190
- initialHeaders["content-type"] = `multipart/form-data; boundary=${boundary}`;
3191
- initialHeaders["content-length"] = String(contentLength);
3192
- return {
3193
- body: Readable.from(multipart()),
3194
- headers: initialHeaders
3195
- };
3284
+ if (!info.isFile() || info.size <= 0) throw invalidArgument("source path must reference a non-empty regular file", "path");
3285
+ const filename = options.filename?.trim() || basename(path);
3286
+ const mimeType = options.mimeType?.trim() || detectQMindMimeType(filename);
3287
+ return Object.freeze({
3288
+ filename,
3289
+ mimeType,
3290
+ async *open() {
3291
+ for await (const chunk of createReadStream(path)) yield chunk;
3292
+ },
3293
+ size: info.size
3294
+ });
3196
3295
  }
3197
- function isTimeoutCause(cause) {
3198
- const code = cause.code;
3199
- const name = cause.name;
3200
- return code === "QMIND_REQUEST_TIMEOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT" || name === "TimeoutError";
3296
+ //#endregion
3297
+ //#region ../node-host/src/host.ts
3298
+ function defaultReporter() {
3299
+ return Object.freeze({
3300
+ info(message) {
3301
+ console.error(message);
3302
+ },
3303
+ warn(message) {
3304
+ console.error(message);
3305
+ }
3306
+ });
3201
3307
  }
3202
- function safeCause(cause) {
3203
- const record = cause;
3204
- return {
3205
- ...typeof record.name === "string" ? { name: record.name } : {},
3206
- ...typeof record.code === "string" ? { code: record.code } : {}
3207
- };
3308
+ function defaultBrowser() {
3309
+ return Object.freeze({ async open(url) {
3310
+ (await open(url, { wait: false })).unref();
3311
+ } });
3208
3312
  }
3209
- function transportFailure(request, cause, deadlineExpired = false) {
3210
- if (cause instanceof QMindError) return cause;
3211
- if (request.signal?.aborted === true) return new QMindError("ABORTED", "QMind request was aborted");
3212
- if (deadlineExpired || isTimeoutCause(cause)) return new QMindError("TIMEOUT", "QMind request timed out", { cause: safeCause(cause) });
3213
- return new QMindError("NETWORK_ERROR", "QMind network request failed", { cause: safeCause(cause) });
3313
+ async function tlsAuthority(path) {
3314
+ if (path === void 0) return void 0;
3315
+ try {
3316
+ return await readFile(path, "utf8");
3317
+ } catch {
3318
+ throw new QMindError("HOST_POLICY_ERROR", "unable to read the configured TLS CA file", { details: { field: "tlsCaFile" } });
3319
+ }
3214
3320
  }
3215
- function requestDeadline(signal, milliseconds) {
3216
- const controller = new AbortController();
3217
- let timedOut = false;
3218
- const timeout = setTimeout(() => {
3219
- timedOut = true;
3220
- const error = Object.assign(/* @__PURE__ */ new Error("QMind request deadline expired"), {
3221
- code: "QMIND_REQUEST_TIMEOUT",
3222
- name: "TimeoutError"
3223
- });
3224
- controller.abort(error);
3225
- }, milliseconds);
3226
- timeout.unref();
3227
- const onAbort = () => controller.abort(signal?.reason);
3228
- if (signal?.aborted === true) onAbort();
3229
- else signal?.addEventListener("abort", onAbort, { once: true });
3230
- const clearDeadline = () => clearTimeout(timeout);
3321
+ function configurationOptions(options, reporter) {
3231
3322
  return {
3232
- clearTimeout: clearDeadline,
3233
- dispose() {
3234
- clearDeadline();
3235
- signal?.removeEventListener("abort", onAbort);
3236
- },
3237
- signal: controller.signal,
3238
- get timedOut() {
3239
- return timedOut;
3240
- }
3323
+ ...options.credentialSashOrigin === void 0 ? {} : { credentialSashOrigin: options.credentialSashOrigin },
3324
+ ...options.environment === void 0 ? {} : { environment: options.environment },
3325
+ ...options.flags === void 0 ? {} : { flags: options.flags },
3326
+ ...options.homeDirectory === void 0 ? {} : { homeDirectory: options.homeDirectory },
3327
+ ...options.legacyEnvironmentPath === void 0 ? {} : { legacyEnvironmentPath: options.legacyEnvironmentPath },
3328
+ ...reporter === void 0 ? {} : { reporter },
3329
+ ...options.stdinIsTTY === void 0 ? {} : { stdinIsTTY: options.stdinIsTTY }
3241
3330
  };
3242
3331
  }
3243
- function abortableSleep(milliseconds, signal) {
3244
- return new Promise((resolve, reject) => {
3245
- if (signal?.aborted === true) {
3246
- reject(signal.reason);
3247
- return;
3332
+ async function createQMindCliHost(options = {}) {
3333
+ const reporter = options.adapters?.reporter ?? defaultReporter();
3334
+ const clock = options.adapters?.clock ?? createSystemClock();
3335
+ const vault = createFileCredentialVault({
3336
+ homeDirectory: (await loadQMindCliConfig(configurationOptions(options))).runtime.homeDirectory,
3337
+ now: () => clock.now(),
3338
+ secretStore: options.adapters?.secretStore ?? createOptionalKeyringSecretStore()
3339
+ });
3340
+ let credentials;
3341
+ try {
3342
+ credentials = await vault.load();
3343
+ } catch (error) {
3344
+ if (options.credentialReadFailure !== "unauthenticated") throw error;
3345
+ reporter.warn("QMind credential vault is unavailable; authentication is required.");
3346
+ credentials = void 0;
3347
+ }
3348
+ const resolved = await loadQMindCliConfig({
3349
+ ...configurationOptions(options, reporter),
3350
+ ...credentials?.sashOrigin ? { credentialSashOrigin: credentials.sashOrigin } : {},
3351
+ reporter
3352
+ });
3353
+ const ca = await tlsAuthority(resolved.tlsCaFile);
3354
+ const nodeOptions = {
3355
+ ...options.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: options.connectTimeoutMs },
3356
+ ...resolved.debug ? { debugLog: (message) => reporter.info(message) } : {},
3357
+ environment: resolved.environment,
3358
+ ...options.maxRetries === void 0 ? {} : { maxRetries: options.maxRetries },
3359
+ origin: resolved.runtime.sashOrigin,
3360
+ proxyFromEnvironment: resolved.proxyFromEnvironment,
3361
+ requestTimeoutMs: resolved.requestTimeoutMs,
3362
+ streamIdleTimeoutMs: resolved.streamIdleTimeoutMs,
3363
+ tls: {
3364
+ ...ca === void 0 ? {} : { ca },
3365
+ rejectUnauthorized: resolved.tlsRejectUnauthorized
3248
3366
  }
3249
- const onAbort = () => {
3250
- clearTimeout(timer);
3251
- reject(signal?.reason);
3252
- };
3253
- const timer = setTimeout(() => {
3254
- signal?.removeEventListener("abort", onAbort);
3255
- resolve();
3256
- }, milliseconds);
3257
- signal?.addEventListener("abort", onAbort, { once: true });
3367
+ };
3368
+ const ownedTransport = options.adapters?.transport === void 0 ? createQMindNodeTransport(nodeOptions) : void 0;
3369
+ const rawTransport = options.adapters?.transport ?? ownedTransport;
3370
+ if (rawTransport === void 0) throw new QMindError("HOST_POLICY_ERROR", "unable to initialize the QMind transport");
3371
+ const auth = createQMindCliAuthManager({
3372
+ browser: options.adapters?.browser ?? defaultBrowser(),
3373
+ clock,
3374
+ config: resolved.runtime,
3375
+ credentials,
3376
+ explicitToken: resolved.token,
3377
+ reporter,
3378
+ transport: rawTransport,
3379
+ vault
3380
+ });
3381
+ const client = createQMindClient({
3382
+ access: "sash",
3383
+ client: resolved.runtime.client,
3384
+ transport: createAuthenticatedQMindTransport(rawTransport, auth)
3385
+ });
3386
+ return Object.freeze({
3387
+ client,
3388
+ async close() {
3389
+ await ownedTransport?.close();
3390
+ },
3391
+ config: resolved.runtime,
3392
+ createFileSource: createQMindFileSource,
3393
+ async downloadToFile(downloadOptions) {
3394
+ const downloader = options.adapters?.downloader ?? ownedTransport;
3395
+ if (downloader === void 0) throw new QMindError("UNSUPPORTED_OPERATION", "the injected CLI transport does not provide file downloads");
3396
+ return await downloader.downloadToFile(downloadOptions);
3397
+ },
3398
+ login: auth.login,
3399
+ logout: auth.logout
3258
3400
  });
3259
3401
  }
3260
- function requestUrl(origin, request) {
3261
- return request.destination.kind === "host" ? new URL(request.destination.path, origin) : new URL(request.destination.url);
3402
+ //#endregion
3403
+ //#region src/folder-sync.ts
3404
+ const QMIND_FOLDER_SYNC_DEFAULT_EXTENSIONS = Object.freeze([
3405
+ ".mdx",
3406
+ ".md",
3407
+ ".pdf",
3408
+ ".txt",
3409
+ ".docx",
3410
+ ".pptx",
3411
+ ".png",
3412
+ ".jpg",
3413
+ ".jpeg",
3414
+ ".gif",
3415
+ ".svg",
3416
+ ".webp"
3417
+ ]);
3418
+ function posix(path) {
3419
+ return path.split(sep).join("/");
3262
3420
  }
3263
- function debugRequest(options, request, url) {
3264
- options.debugLog?.(`[qmind-debug] ${request.method} ${url.origin}${url.pathname}`);
3421
+ function qmindFolderSyncIndexKey(parentId, title) {
3422
+ return `${parentId}\u0000${title}`;
3265
3423
  }
3266
- async function* decodedEvents(body, request, disposeSignal) {
3267
- const decoder = new TextDecoder("utf-8", { fatal: true });
3268
- const pending = [];
3269
- let done = false;
3270
- let parserFailure;
3271
- const parser = createParser({
3272
- maxBufferSize: SSE_MAX_BUFFER_SIZE,
3273
- onError(error) {
3274
- if (error.type === "max-buffer-size-exceeded") parserFailure = error;
3275
- },
3276
- onEvent(event) {
3277
- if (event.data.trim() === "[DONE]") done = true;
3278
- else pending.push(event.data);
3279
- }
3280
- });
3281
- const drain = function* () {
3282
- while (pending.length > 0) {
3283
- const value = pending.shift();
3284
- if (value !== void 0) yield value;
3285
- }
3286
- };
3287
- try {
3288
- for await (const chunk of body) {
3289
- parser.feed(decoder.decode(chunk, { stream: true }));
3290
- if (parserFailure !== void 0) throw new QMindError("STREAM_ERROR", "SSE parser buffer limit exceeded", { details: { limitBytes: SSE_MAX_BUFFER_SIZE } });
3291
- yield* drain();
3292
- if (done) return;
3293
- }
3294
- parser.feed(decoder.decode());
3295
- parser.reset({ consume: true });
3296
- if (parserFailure !== void 0) throw new QMindError("STREAM_ERROR", "SSE parser buffer limit exceeded", { details: { limitBytes: SSE_MAX_BUFFER_SIZE } });
3297
- yield* drain();
3298
- } catch (cause) {
3299
- if (cause instanceof QMindError) throw cause;
3300
- if (request.signal?.aborted === true) throw new QMindError("ABORTED", "QMind stream was aborted");
3301
- if (isTimeoutCause(cause)) throw new QMindError("TIMEOUT", "QMind stream timed out", { cause: safeCause(cause) });
3302
- throw new QMindError("STREAM_ERROR", "QMind SSE stream failed", { cause: safeCause(cause) });
3303
- } finally {
3304
- disposeSignal();
3305
- body.destroy();
3306
- }
3424
+ function normalizeExtensions(values) {
3425
+ return new Set(values.map((value) => value.trim().toLowerCase()).filter((value) => value.length > 0).map((value) => value.startsWith(".") ? value : `.${value}`));
3307
3426
  }
3308
- function createQMindNodeTransport(options) {
3309
- const origin = normalizeOrigin(options.origin);
3310
- const requestTimeoutMs = positiveInteger(options.requestTimeoutMs, 3e4, "requestTimeoutMs");
3311
- const streamIdleTimeoutMs = positiveInteger(options.streamIdleTimeoutMs, 12e4, "streamIdleTimeoutMs");
3312
- const maxRetries = nonNegativeInteger(options.maxRetries, 2, "maxRetries");
3313
- const dispatchers = createDispatchers(options);
3314
- const dispatcherFor = (request) => request.destination.kind === "host" ? dispatchers.host : dispatchers.signed;
3427
+ function validateMapping(value) {
3428
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new QMindError("INVALID_ARGUMENT", "directory ID mapping must be a JSON object");
3429
+ const result = {};
3430
+ for (const [path, id] of Object.entries(value)) {
3431
+ if (typeof id !== "string" || id.trim().length === 0) throw new QMindError("INVALID_ARGUMENT", "directory ID mapping values must be non-empty strings");
3432
+ result[posix(path)] = id.trim();
3433
+ }
3434
+ return result;
3435
+ }
3436
+ async function sha256(path) {
3437
+ const hash = createHash("sha256");
3438
+ for await (const chunk of createReadStream(path)) hash.update(chunk);
3439
+ return hash.digest("hex");
3440
+ }
3441
+ function createNodeQMindFolderSyncFileSystem() {
3315
3442
  return Object.freeze({
3316
- async close() {
3317
- const unique = /* @__PURE__ */ new Set([dispatchers.host, dispatchers.signed]);
3318
- await Promise.all([...unique].map(async (dispatcher) => dispatcher.close()));
3443
+ async isDirectory(path) {
3444
+ return (await stat(path).catch(() => void 0))?.isDirectory() === true;
3319
3445
  },
3320
- async downloadToFile(download) {
3321
- const request$1 = {
3322
- destination: {
3323
- kind: "signed",
3324
- url: download.url
3325
- },
3326
- idempotent: true,
3327
- method: "GET",
3328
- ...download.signal === void 0 ? {} : { signal: download.signal }
3329
- };
3330
- assertWireRequest(request$1);
3331
- const url = requestUrl(origin, request$1);
3332
- await mkdir(dirname(download.targetPath), { recursive: true });
3333
- if (download.overwrite !== true) try {
3334
- await stat(download.targetPath);
3335
- throw new QMindError("CONFLICT", `target already exists: ${download.targetPath}`);
3336
- } catch (error) {
3337
- if (error.code !== "ENOENT") throw error;
3338
- }
3339
- const temporary = join(dirname(download.targetPath), `.${basename(download.targetPath)}.${randomBytes(8).toString("hex")}.tmp`);
3340
- let response;
3446
+ async readMapping(path) {
3341
3447
  try {
3342
- response = await request(url, {
3343
- bodyTimeout: requestTimeoutMs,
3344
- dispatcher: dispatchers.signed,
3345
- headersTimeout: requestTimeoutMs,
3346
- idempotent: true,
3347
- method: "GET",
3348
- ...download.signal === void 0 ? {} : { signal: download.signal }
3349
- });
3350
- if (response.statusCode < 200 || response.statusCode >= 300) {
3351
- const decoded = await wireResponse(response);
3352
- throw new QMindError("REMOTE_ERROR", "download request failed", {
3353
- ...decoded.rawBody === void 0 ? {} : { rawBody: decoded.rawBody },
3354
- status: decoded.status
3355
- });
3356
- }
3357
- await pipeline(response.body, createWriteStream(temporary, {
3358
- flags: "wx",
3359
- mode: 384
3360
- }));
3361
- const size = (await stat(temporary)).size;
3362
- await chmod(temporary, 420);
3363
- await commitQMindTemporaryFile(temporary, download.targetPath, download.overwrite === true);
3364
- return size;
3365
- } catch (cause) {
3366
- response?.body.destroy();
3367
- await rm(temporary, { force: true });
3368
- if (cause instanceof QMindError) throw cause;
3369
- throw transportFailure(request$1, cause);
3448
+ return validateMapping(JSON.parse(await readFile(path, "utf8")));
3449
+ } catch (error) {
3450
+ if (error instanceof QMindError) throw error;
3451
+ throw new QMindError("INVALID_ARGUMENT", `unable to read directory ID mapping: ${path}`);
3370
3452
  }
3371
3453
  },
3372
- async request(request$2) {
3373
- assertWireRequest(request$2);
3374
- const url = requestUrl(origin, request$2);
3375
- const attempts = request$2.idempotent ? maxRetries + 1 : 1;
3376
- let lastFailure;
3377
- let lastDeadlineExpired = false;
3378
- for (let attempt = 0; attempt < attempts; attempt += 1) {
3379
- const deadline = requestDeadline(request$2.signal, request$2.timeoutMs ?? requestTimeoutMs);
3380
- try {
3381
- debugRequest(options, request$2, url);
3382
- const encoded = encodeBody(request$2, `----qmind-${randomBytes(18).toString("hex")}`);
3383
- const response = await request(url, {
3384
- ...encoded.body === void 0 ? {} : { body: encoded.body },
3385
- bodyTimeout: request$2.timeoutMs ?? requestTimeoutMs,
3386
- dispatcher: dispatcherFor(request$2),
3387
- headers: encoded.headers,
3388
- headersTimeout: request$2.timeoutMs ?? requestTimeoutMs,
3389
- idempotent: request$2.idempotent,
3390
- method: request$2.method,
3391
- signal: deadline.signal
3392
- });
3393
- if (attempt + 1 < attempts && RETRYABLE_STATUS.has(response.statusCode)) {
3394
- await response.body.dump();
3395
- await abortableSleep(250 * 2 ** attempt, request$2.signal);
3454
+ async scan(baseDirectory, options) {
3455
+ const directories = [];
3456
+ const files = [];
3457
+ let skipped = 0;
3458
+ const isIgnored = options.ignorePatterns.length === 0 ? () => false : picomatch([...options.ignorePatterns], { dot: true });
3459
+ const visit = async (directory) => {
3460
+ const entries = await readdir(directory, { withFileTypes: true });
3461
+ entries.sort((left, right) => left.name.localeCompare(right.name));
3462
+ for (const entry of entries) {
3463
+ const absolute = join(directory, entry.name);
3464
+ const path = posix(relative(baseDirectory, absolute));
3465
+ if (entry.name.startsWith(".") || isIgnored(path)) {
3466
+ skipped += entry.isFile() ? 1 : 0;
3396
3467
  continue;
3397
3468
  }
3398
- return await wireResponse(response);
3399
- } catch (cause) {
3400
- lastFailure = cause;
3401
- lastDeadlineExpired = deadline.timedOut;
3402
- if (attempt + 1 < attempts && request$2.signal?.aborted !== true) {
3403
- await abortableSleep(250 * 2 ** attempt, request$2.signal);
3404
- continue;
3469
+ if (entry.isDirectory()) {
3470
+ directories.push(path);
3471
+ await visit(absolute);
3472
+ } else if (entry.isFile()) {
3473
+ if (options.extensions.has(extname(entry.name).toLowerCase())) files.push(path);
3474
+ else skipped += 1;
3405
3475
  }
3406
- } finally {
3407
- deadline.dispose();
3408
3476
  }
3477
+ };
3478
+ await visit(baseDirectory);
3479
+ directories.sort((left, right) => left.split("/").length - right.split("/").length || left.localeCompare(right));
3480
+ files.sort((left, right) => left.localeCompare(right));
3481
+ return {
3482
+ directories,
3483
+ files,
3484
+ skipped
3485
+ };
3486
+ },
3487
+ sha256,
3488
+ async writeMapping(path, mapping) {
3489
+ await mkdir(dirname(path), { recursive: true });
3490
+ await writeFile(path, `${JSON.stringify(mapping, null, 2)}\n`, { mode: 384 });
3491
+ }
3492
+ });
3493
+ }
3494
+ async function shouldReplaceQMindFolderFile(mode, existing, localPath, fileSystem = createNodeQMindFolderSyncFileSystem()) {
3495
+ if (mode === "skip") return false;
3496
+ if (mode === "overwrite") return true;
3497
+ const metadata = existing.metadata ?? {};
3498
+ const remoteHash = metadata.fileSha256 ?? metadata.rawFileSha256 ?? metadata.sha256;
3499
+ if (typeof remoteHash !== "string" || !/^[a-f0-9]{64}$/i.test(remoteHash)) return true;
3500
+ return (await fileSystem.sha256(localPath)).toLowerCase() !== remoteHash.toLowerCase();
3501
+ }
3502
+ function retryable(error) {
3503
+ if (!(error instanceof QMindError)) return true;
3504
+ if (error.code === "FILE_TOO_LARGE" || error.code === "INVALID_ARGUMENT" || error.code === "AUTH_REQUIRED" || error.code === "FORBIDDEN") return false;
3505
+ return error.status === void 0 || error.status === 408 || error.status === 429 || error.status >= 500;
3506
+ }
3507
+ async function withUploadRetry(task, sleep) {
3508
+ let lastFailure;
3509
+ for (let attempt = 0; attempt < 3; attempt += 1) try {
3510
+ return await task();
3511
+ } catch (error) {
3512
+ lastFailure = error;
3513
+ if (!retryable(error) || attempt === 2) break;
3514
+ await sleep(250 * 2 ** attempt);
3515
+ }
3516
+ throw lastFailure;
3517
+ }
3518
+ function defaultSleep(milliseconds) {
3519
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
3520
+ }
3521
+ function mappingPath(baseDirectory, cacheDirectory) {
3522
+ const cache = cacheDirectory ?? process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
3523
+ const name = createHash("sha1").update(baseDirectory).digest("hex").slice(0, 16);
3524
+ return join(cache, "qmind", "dir-ids", `${name}.json`);
3525
+ }
3526
+ function addFailure(stats, field, path) {
3527
+ (stats[field] ??= []).push(path);
3528
+ }
3529
+ function publicStats(stats) {
3530
+ return Object.freeze({
3531
+ ...stats,
3532
+ ...stats.failed_dirs === void 0 ? {} : { failed_dirs: Object.freeze(stats.failed_dirs) },
3533
+ ...stats.failed_files === void 0 ? {} : { failed_files: Object.freeze(stats.failed_files) },
3534
+ ...stats.failed_deletes === void 0 ? {} : { failed_deletes: Object.freeze(stats.failed_deletes) }
3535
+ });
3536
+ }
3537
+ async function synchronizeQMindFolder(host, options) {
3538
+ const notebookId = options.notebookId.trim();
3539
+ if (notebookId.length === 0) throw new QMindError("INVALID_ARGUMENT", "notebookId is required");
3540
+ const fileSystem = options.fileSystem ?? createNodeQMindFolderSyncFileSystem();
3541
+ const baseDirectory = resolve(options.baseDirectory);
3542
+ if (!await fileSystem.isDirectory(baseDirectory)) throw new QMindError("INVALID_ARGUMENT", `${baseDirectory} is not a directory`);
3543
+ const mode = options.mode ?? "skip";
3544
+ if (options.skipUpload === true && options.delete === true) throw new QMindError("INVALID_ARGUMENT", "delete cannot be combined with skipUpload because local files would not enter the mirror plan");
3545
+ const requestedConcurrency = options.concurrency ?? 3;
3546
+ if (!Number.isSafeInteger(requestedConcurrency) || requestedConcurrency < 1) throw new QMindError("INVALID_ARGUMENT", "concurrency must be a positive integer");
3547
+ const concurrency = Math.min(10, requestedConcurrency);
3548
+ const diagnostics = [];
3549
+ if (requestedConcurrency > 10) diagnostics.push(`warning: concurrency ${requestedConcurrency} exceeds maximum (10); clamped to 10`);
3550
+ const extensions = normalizeExtensions(options.extensions ?? QMIND_FOLDER_SYNC_DEFAULT_EXTENSIONS);
3551
+ const tree = await fileSystem.scan(baseDirectory, {
3552
+ extensions,
3553
+ ignorePatterns: options.ignorePatterns ?? []
3554
+ });
3555
+ const remote = await host.client.listAllSources(notebookId);
3556
+ const remoteIndex = new Map(remote.map((source) => [qmindFolderSyncIndexKey(source.parentSourceId, source.title), source]));
3557
+ const remoteById = new Map(remote.map((source) => [source.id, source]));
3558
+ const stats = {
3559
+ dirs_created: 0,
3560
+ dirs_reused: 0,
3561
+ dirs_failed: 0,
3562
+ dirs_refresh_failed: 0,
3563
+ files_uploaded: 0,
3564
+ files_skipped: options.skipUpload === true ? 0 : tree.skipped,
3565
+ files_already_exist: 0,
3566
+ files_failed: 0,
3567
+ ...options.delete === true ? {
3568
+ delete_failed: 0,
3569
+ dirs_deleted: 0,
3570
+ files_deleted: 0
3571
+ } : {},
3572
+ ...options.dryRun === true ? { dry_run: true } : {}
3573
+ };
3574
+ const configuredMapping = options.directoryIdsPath === void 0 ? {} : await fileSystem.readMapping(options.directoryIdsPath);
3575
+ const directoryIds = {};
3576
+ const desiredKeys = /* @__PURE__ */ new Set();
3577
+ for (const directory of tree.directories) {
3578
+ const parentPath = posix(dirname(directory)) === "." ? "" : posix(dirname(directory));
3579
+ const parentId = directoryIds[parentPath] ?? "";
3580
+ if (parentPath.length > 0 && parentId.length === 0) {
3581
+ stats.dirs_failed += 1;
3582
+ addFailure(stats, "failed_dirs", directory);
3583
+ continue;
3584
+ }
3585
+ const title = basename(directory);
3586
+ const key = qmindFolderSyncIndexKey(parentId, title);
3587
+ desiredKeys.add(key);
3588
+ const mappedId = configuredMapping[directory];
3589
+ const mapped = mappedId === void 0 ? void 0 : remoteById.get(mappedId);
3590
+ const existing = mappedId === void 0 || mapped === void 0 ? remoteIndex.get(key) : mapped;
3591
+ if (existing?.isDir === true && existing.parentSourceId === parentId && existing.title === title) {
3592
+ directoryIds[directory] = existing.id;
3593
+ stats.dirs_reused += 1;
3594
+ continue;
3595
+ }
3596
+ if (existing !== void 0) {
3597
+ stats.dirs_failed += 1;
3598
+ addFailure(stats, "failed_dirs", directory);
3599
+ continue;
3600
+ }
3601
+ if (options.dryRun === true) {
3602
+ directoryIds[directory] = `dry-run:${directory}`;
3603
+ stats.dirs_created += 1;
3604
+ continue;
3605
+ }
3606
+ try {
3607
+ const created = await host.client.createDirectory(notebookId, {
3608
+ parentSourceId: parentId,
3609
+ path: title,
3610
+ title
3611
+ });
3612
+ directoryIds[directory] = created.id;
3613
+ remoteIndex.set(key, created);
3614
+ remoteById.set(created.id, created);
3615
+ stats.dirs_created += 1;
3616
+ } catch {
3617
+ stats.dirs_failed += 1;
3618
+ addFailure(stats, "failed_dirs", directory);
3619
+ }
3620
+ }
3621
+ let savedMappingPath;
3622
+ if (options.directoryIdsPath === void 0 && options.dryRun !== true) {
3623
+ savedMappingPath = options.saveDirectoryIdsPath ?? mappingPath(baseDirectory, options.cacheDirectory);
3624
+ await fileSystem.writeMapping(savedMappingPath, directoryIds);
3625
+ diagnostics.push(`directory mapping saved: ${savedMappingPath}`);
3626
+ }
3627
+ if (options.skipUpload !== true) {
3628
+ const limit = pLimit(concurrency);
3629
+ await Promise.all(tree.files.map((file) => limit(async () => {
3630
+ const absolute = join(baseDirectory, file);
3631
+ const parentPath = posix(dirname(file)) === "." ? "" : posix(dirname(file));
3632
+ const parentId = directoryIds[parentPath] ?? "";
3633
+ if (parentPath.length > 0 && parentId.length === 0) {
3634
+ stats.files_failed += 1;
3635
+ addFailure(stats, "failed_files", file);
3636
+ return;
3409
3637
  }
3410
- throw transportFailure(request$2, lastFailure, lastDeadlineExpired);
3411
- },
3412
- async stream(request$3) {
3413
- assertWireRequest(request$3);
3414
- const url = requestUrl(origin, request$3);
3415
- debugRequest(options, request$3, url);
3416
- const encoded = encodeBody(request$3, `----qmind-${randomBytes(18).toString("hex")}`);
3417
- let response;
3418
- const deadline = requestDeadline(request$3.signal, request$3.timeoutMs ?? requestTimeoutMs);
3419
- try {
3420
- response = await request(url, {
3421
- ...encoded.body === void 0 ? {} : { body: encoded.body },
3422
- bodyTimeout: streamIdleTimeoutMs,
3423
- dispatcher: dispatchers.host,
3424
- headers: {
3425
- accept: "text/event-stream",
3426
- ...encoded.headers
3427
- },
3428
- headersTimeout: request$3.timeoutMs ?? requestTimeoutMs,
3429
- idempotent: request$3.idempotent,
3430
- method: request$3.method,
3431
- signal: deadline.signal
3432
- });
3433
- } catch (cause) {
3434
- deadline.dispose();
3435
- throw transportFailure(request$3, cause, deadline.timedOut);
3638
+ const title = basename(file);
3639
+ const key = qmindFolderSyncIndexKey(parentId, title);
3640
+ desiredKeys.add(key);
3641
+ const existing = remoteIndex.get(key);
3642
+ if (existing?.isDir === true) {
3643
+ stats.files_failed += 1;
3644
+ addFailure(stats, "failed_files", file);
3645
+ return;
3436
3646
  }
3437
- deadline.clearTimeout();
3438
- const headers = headersToRecord(response.headers);
3439
- if (response.statusCode < 200 || response.statusCode >= 300 || !headers["content-type"]?.toLowerCase().includes("text/event-stream")) try {
3440
- return {
3441
- ...await wireResponse(response),
3442
- kind: "response"
3443
- };
3444
- } finally {
3445
- deadline.dispose();
3647
+ if (existing !== void 0 && !await shouldReplaceQMindFolderFile(mode, existing, absolute, fileSystem)) {
3648
+ stats.files_already_exist += 1;
3649
+ return;
3446
3650
  }
3447
- return {
3448
- events: decodedEvents(response.body, request$3, deadline.dispose),
3449
- headers,
3450
- kind: "events",
3451
- status: response.statusCode
3452
- };
3651
+ if (options.dryRun === true) {
3652
+ stats.files_uploaded += 1;
3653
+ return;
3654
+ }
3655
+ try {
3656
+ const hash = await fileSystem.sha256(absolute);
3657
+ const uploaded = await withUploadRetry(async () => {
3658
+ const binary = await host.createFileSource(absolute);
3659
+ return await host.client.uploadSource(notebookId, binary, {
3660
+ parentSourceId: parentId,
3661
+ path: file,
3662
+ sha256: hash,
3663
+ title
3664
+ });
3665
+ }, options.sleep ?? defaultSleep);
3666
+ if (existing !== void 0) await host.client.deleteSource(notebookId, existing.id);
3667
+ remoteIndex.set(key, uploaded);
3668
+ remoteById.set(uploaded.id, uploaded);
3669
+ stats.files_uploaded += 1;
3670
+ } catch {
3671
+ stats.files_failed += 1;
3672
+ addFailure(stats, "failed_files", file);
3673
+ }
3674
+ })));
3675
+ }
3676
+ if (options.dryRun !== true) for (const [directory, id] of Object.entries(directoryIds)) {
3677
+ if (id.startsWith("dry-run:")) continue;
3678
+ try {
3679
+ await host.client.updateSource(notebookId, id, {
3680
+ isDir: true,
3681
+ title: basename(directory)
3682
+ });
3683
+ } catch {
3684
+ stats.dirs_refresh_failed += 1;
3453
3685
  }
3454
- });
3455
- }
3456
- const MIME_TYPES = Object.freeze({
3457
- ".csv": "text/csv",
3458
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
3459
- ".gif": "image/gif",
3460
- ".jpeg": "image/jpeg",
3461
- ".jpg": "image/jpeg",
3462
- ".md": "text/markdown",
3463
- ".mdx": "text/markdown",
3464
- ".pdf": "application/pdf",
3465
- ".png": "image/png",
3466
- ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
3467
- ".svg": "image/svg+xml",
3468
- ".txt": "text/plain",
3469
- ".webp": "image/webp",
3470
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
3471
- });
3472
- function detectQMindMimeType(filename) {
3473
- return MIME_TYPES[extname(filename).toLowerCase()] ?? "application/octet-stream";
3474
- }
3475
- async function createQMindFileSource(path, options = {}) {
3476
- let info;
3477
- try {
3478
- info = await stat(path);
3479
- } catch {
3480
- throw invalidArgument("source path must reference a readable file", "path");
3481
3686
  }
3482
- if (!info.isFile() || info.size <= 0) throw invalidArgument("source path must reference a non-empty regular file", "path");
3483
- const filename = options.filename?.trim() || basename(path);
3484
- const mimeType = options.mimeType?.trim() || detectQMindMimeType(filename);
3485
- return Object.freeze({
3486
- filename,
3487
- mimeType,
3488
- async *open() {
3489
- for await (const chunk of createReadStream(path)) yield chunk;
3490
- },
3491
- size: info.size
3492
- });
3493
- }
3494
- //#endregion
3495
- //#region src/host.ts
3496
- function defaultReporter() {
3497
- return Object.freeze({
3498
- info(message) {
3499
- console.error(message);
3500
- },
3501
- warn(message) {
3502
- console.error(message);
3687
+ const preDeleteFailures = stats.files_failed + stats.dirs_failed;
3688
+ if (options.delete === true && preDeleteFailures === 0) {
3689
+ const deletions = remote.filter((source) => !desiredKeys.has(qmindFolderSyncIndexKey(source.parentSourceId, source.title))).sort((left, right) => {
3690
+ if (left.isDir !== right.isDir) return left.isDir ? 1 : -1;
3691
+ return right.path.split("/").length - left.path.split("/").length;
3692
+ });
3693
+ for (const source of deletions) try {
3694
+ if (options.dryRun !== true) await host.client.deleteSource(notebookId, source.id);
3695
+ if (source.isDir) stats.dirs_deleted = (stats.dirs_deleted ?? 0) + 1;
3696
+ else stats.files_deleted = (stats.files_deleted ?? 0) + 1;
3697
+ } catch {
3698
+ stats.delete_failed = (stats.delete_failed ?? 0) + 1;
3699
+ (stats.failed_deletes ??= []).push(source.path || source.title);
3503
3700
  }
3504
- });
3505
- }
3506
- function defaultBrowser() {
3507
- return Object.freeze({ async open(url) {
3508
- (await open(url, { wait: false })).unref();
3509
- } });
3510
- }
3511
- async function tlsAuthority(path) {
3512
- if (path === void 0) return void 0;
3513
- try {
3514
- return await readFile(path, "utf8");
3515
- } catch {
3516
- throw new QMindError("HOST_POLICY_ERROR", "unable to read the configured TLS CA file", { details: { field: "tlsCaFile" } });
3517
3701
  }
3518
- }
3519
- function configurationOptions(options, reporter) {
3702
+ const exitCode = stats.files_failed > 0 || stats.dirs_failed > 0 || stats.dirs_refresh_failed > 0 || (stats.delete_failed ?? 0) > 0 ? 1 : 0;
3520
3703
  return {
3521
- ...options.credentialSashOrigin === void 0 ? {} : { credentialSashOrigin: options.credentialSashOrigin },
3522
- ...options.environment === void 0 ? {} : { environment: options.environment },
3523
- ...options.flags === void 0 ? {} : { flags: options.flags },
3524
- ...options.homeDirectory === void 0 ? {} : { homeDirectory: options.homeDirectory },
3525
- ...options.legacyEnvironmentPath === void 0 ? {} : { legacyEnvironmentPath: options.legacyEnvironmentPath },
3526
- ...reporter === void 0 ? {} : { reporter },
3527
- ...options.stdinIsTTY === void 0 ? {} : { stdinIsTTY: options.stdinIsTTY }
3528
- };
3529
- }
3530
- async function createQMindCliHost(options = {}) {
3531
- const reporter = options.adapters?.reporter ?? defaultReporter();
3532
- const clock = options.adapters?.clock ?? createSystemClock();
3533
- const vault = createFileCredentialVault({
3534
- homeDirectory: (await loadQMindCliConfig(configurationOptions(options))).runtime.homeDirectory,
3535
- now: () => clock.now(),
3536
- secretStore: options.adapters?.secretStore ?? createOptionalKeyringSecretStore()
3537
- });
3538
- const credentials = await vault.load();
3539
- const resolved = await loadQMindCliConfig({
3540
- ...configurationOptions(options, reporter),
3541
- ...credentials?.sashOrigin ? { credentialSashOrigin: credentials.sashOrigin } : {},
3542
- reporter
3543
- });
3544
- const ca = await tlsAuthority(resolved.tlsCaFile);
3545
- const nodeOptions = {
3546
- ...options.connectTimeoutMs === void 0 ? {} : { connectTimeoutMs: options.connectTimeoutMs },
3547
- ...resolved.debug ? { debugLog: (message) => reporter.info(message) } : {},
3548
- environment: resolved.environment,
3549
- ...options.maxRetries === void 0 ? {} : { maxRetries: options.maxRetries },
3550
- origin: resolved.runtime.sashOrigin,
3551
- proxyFromEnvironment: resolved.proxyFromEnvironment,
3552
- requestTimeoutMs: resolved.requestTimeoutMs,
3553
- streamIdleTimeoutMs: resolved.streamIdleTimeoutMs,
3554
- tls: {
3555
- ...ca === void 0 ? {} : { ca },
3556
- rejectUnauthorized: resolved.tlsRejectUnauthorized
3557
- }
3704
+ diagnostics: Object.freeze(diagnostics),
3705
+ exitCode,
3706
+ ...savedMappingPath === void 0 ? {} : { mappingPath: savedMappingPath },
3707
+ stats: publicStats(stats)
3558
3708
  };
3559
- const ownedTransport = options.adapters?.transport === void 0 ? createQMindNodeTransport(nodeOptions) : void 0;
3560
- const rawTransport = options.adapters?.transport ?? ownedTransport;
3561
- if (rawTransport === void 0) throw new QMindError("HOST_POLICY_ERROR", "unable to initialize the QMind transport");
3562
- const auth = createQMindCliAuthManager({
3563
- browser: options.adapters?.browser ?? defaultBrowser(),
3564
- clock,
3565
- config: resolved.runtime,
3566
- credentials,
3567
- explicitToken: resolved.token,
3568
- reporter,
3569
- transport: rawTransport,
3570
- vault
3571
- });
3572
- const client = createQMindClient({
3573
- access: "sash",
3574
- client: resolved.runtime.client,
3575
- transport: createAuthenticatedQMindTransport(rawTransport, auth),
3576
- userId: resolved.runtime.userId
3577
- });
3578
- return Object.freeze({
3579
- client,
3580
- async close() {
3581
- await ownedTransport?.close();
3582
- },
3583
- config: resolved.runtime,
3584
- createFileSource: createQMindFileSource,
3585
- async downloadToFile(downloadOptions) {
3586
- const downloader = options.adapters?.downloader ?? ownedTransport;
3587
- if (downloader === void 0) throw new QMindError("UNSUPPORTED_OPERATION", "the injected CLI transport does not provide file downloads");
3588
- return await downloader.downloadToFile(downloadOptions);
3589
- },
3590
- login: auth.login,
3591
- logout: auth.logout
3592
- });
3593
3709
  }
3594
3710
  //#endregion
3595
3711
  //#region src/output.ts
@@ -4435,14 +4551,12 @@ function commonFlags(options) {
4435
4551
  const dashboard = optionalString(options, "dashboard");
4436
4552
  const sash = optionalString(options, "sash");
4437
4553
  const token = optionalString(options, "token");
4438
- const user = optionalString(options, "user");
4439
4554
  return {
4440
4555
  ...client === void 0 ? {} : { client },
4441
4556
  ...dashboard === void 0 ? {} : { dashboard },
4442
4557
  ...options.nonInteractive === true ? { nonInteractive: true } : {},
4443
4558
  ...sash === void 0 ? {} : { sash },
4444
- ...token === void 0 ? {} : { token },
4445
- ...user === void 0 ? {} : { user }
4559
+ ...token === void 0 ? {} : { token }
4446
4560
  };
4447
4561
  }
4448
4562
  async function hostFor(context, options, environmentOverrides = {}) {
@@ -4475,7 +4589,7 @@ function render(context, options, presentation) {
4475
4589
  context.io.stdout(renderQMindCliPresentation(presentation, formatOf(options)));
4476
4590
  }
4477
4591
  function withCommon(command, output = true) {
4478
- command.option("--token <token>", "Bearer access token").option("--sash <url>", "Sash server origin").option("--dashboard <url>", "Dashboard origin").option("--org <id>", "organization ID (deprecated and ignored)").option("--user <id>", "user ID (direct-mind mode)").option("--client <name>", "originating client identifier").option("--non-interactive", "never open a browser when credentials are missing");
4592
+ command.option("--token <token>", "Bearer access token").option("--sash <url>", "Sash server origin").option("--dashboard <url>", "Dashboard origin").option("--org <id>", "organization ID (deprecated and ignored)").option("--client <name>", "originating client identifier").option("--non-interactive", "never open a browser when credentials are missing");
4479
4593
  if (output) command.option("--format <format>", "output format: table | json | agent | ndjson", "table");
4480
4594
  return command;
4481
4595
  }
@@ -4833,7 +4947,7 @@ function registerUpdateCommand(context, program) {
4833
4947
  publicKey: String(raw.publicKey ?? "")
4834
4948
  });
4835
4949
  if (result.updated) context.io.stdout(`[self-update] updated ${QMIND_CLI_VERSION} -> ${result.version}\n`);
4836
- else if (result.version === "1.1.2".replace(/^v/, "")) context.io.stdout(`[self-update] already current: ${result.version}\n`);
4950
+ else if (result.version === "2.0.0".replace(/^v/, "")) context.io.stdout(`[self-update] already current: ${result.version}\n`);
4837
4951
  else context.io.stdout(`[self-update] verified available version: ${result.version}\n`);
4838
4952
  });
4839
4953
  }