@im-nobsidian/core 0.21.0 → 0.22.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/index.js CHANGED
@@ -3753,7 +3753,7 @@ var NotionClient2 = class _NotionClient {
3753
3753
  return blocks;
3754
3754
  }
3755
3755
  /**
3756
- * 자식 블록을 덧붙인다. `options.after` 를 주면 그 블록 **바로 뒤**에 삽입한다
3756
+ * 자식 블록을 덧붙인다. `options.after` 를 주면 그 블록 **바로 뒤**에, `options.atStart` 면 맨 앞에 삽입한다
3757
3757
  * (기본은 맨 끝). 자리표시자를 실제 미디어 블록으로 제자리 교체할 때 필요하다.
3758
3758
  *
3759
3759
  * 실측(2025-09-03): position 삽입 응답의 `results` 는 방금 넣은 블록뿐 아니라
@@ -3771,19 +3771,24 @@ var NotionClient2 = class _NotionClient {
3771
3771
  const batchSize = this.batchSize;
3772
3772
  const created = [];
3773
3773
  let after = options?.after;
3774
+ let atStart = after === void 0 && options?.atStart === true;
3774
3775
  for (let i = 0; i < children.length; i += batchSize) {
3775
3776
  const batch = children.slice(i, i + batchSize);
3777
+ const position = after ? { position: { type: "after_block", after_block: { id: after } } } : atStart ? { position: { type: "start" } } : {};
3776
3778
  const response = await this.withRateLimit(
3777
3779
  () => this.client.blocks.children.append({
3778
3780
  block_id: blockId,
3779
3781
  children: batch,
3780
- ...after ? { position: { type: "after_block", after_block: { id: after } } } : {}
3782
+ ...position
3781
3783
  }),
3782
3784
  CREATES_BLOCKS
3783
3785
  );
3784
3786
  const blocks = response.results.slice(0, batch.length);
3785
3787
  created.push(...blocks);
3786
- if (after && blocks.length > 0) after = blocks[blocks.length - 1].id;
3788
+ if ((after || atStart) && blocks.length > 0) {
3789
+ after = blocks[blocks.length - 1].id;
3790
+ atStart = false;
3791
+ }
3787
3792
  }
3788
3793
  return created;
3789
3794
  }
@@ -3792,9 +3797,11 @@ var NotionClient2 = class _NotionClient {
3792
3797
  * 자식은 부모를 만든 뒤 그 아래에 마저 붙인다({@link shapeAppendRequests}). 더 깊은 블록의 id 는
3793
3798
  * 응답에 없어 부모의 자식 목록을 읽어 찾는다.
3794
3799
  *
3800
+ * `options.atStart` 면 맨 위 블록을 맨 앞에 넣는다 — 본문 뒤에 둘 블록(폴더 첨부)을 남기고 본문만 바꿀 때.
3801
+ *
3795
3802
  * 중간에 실패하면 그때까지 만든 블록은 남는다 — 호출측이 실패로 알린다.
3796
3803
  */
3797
- async appendBlockTree(parentId, blocks) {
3804
+ async appendBlockTree(parentId, blocks, options) {
3798
3805
  const listed = /* @__PURE__ */ new Map();
3799
3806
  const childIds = async (id) => {
3800
3807
  let ids = listed.get(id);
@@ -3805,15 +3812,19 @@ var NotionClient2 = class _NotionClient {
3805
3812
  return ids;
3806
3813
  };
3807
3814
  const queue = [
3808
- { parentId, blocks }
3815
+ { parentId, blocks, top: true }
3809
3816
  ];
3817
+ let topAfter;
3810
3818
  while (queue.length > 0) {
3811
3819
  const next = queue.shift();
3812
3820
  for (const request of shapeAppendRequests(next.blocks)) {
3821
+ const position = next.top && options?.atStart ? topAfter ? { after: topAfter } : { atStart: true } : void 0;
3813
3822
  const created = await this.appendChildren(
3814
3823
  next.parentId,
3815
- request.map((shaped) => shaped.body)
3824
+ request.map((shaped) => shaped.body),
3825
+ position
3816
3826
  );
3827
+ if (position) topAfter = created[created.length - 1] ?? topAfter;
3817
3828
  for (const [k, shaped] of request.entries()) {
3818
3829
  for (const pending of shaped.pending) {
3819
3830
  let id = created[k];
@@ -15174,6 +15185,21 @@ function selectStaleDbArtifacts(files, localFolder, keepPaths) {
15174
15185
  return stale;
15175
15186
  }
15176
15187
 
15188
+ // src/sync/parent-mode.ts
15189
+ function isDatabaseMode(config) {
15190
+ return config.notion.parentMode === "database" && !!config.notion.databaseId;
15191
+ }
15192
+ function rowDatabaseOf(config, record) {
15193
+ if (record.fileType === "db-row") {
15194
+ if (!record.notionParentId) {
15195
+ throw new Error(`DB \uD589\uC778\uB370 \uC18C\uC18D DB \uB97C \uC54C \uC218 \uC5C6\uC74C: ${record.obsidianPath}`);
15196
+ }
15197
+ return record.notionParentId;
15198
+ }
15199
+ if (isDatabaseMode(config)) return config.notion.databaseId;
15200
+ return null;
15201
+ }
15202
+
15177
15203
  // src/sync/folder-files.ts
15178
15204
  function unescapeCaption(caption) {
15179
15205
  return caption.replace(/\\([\\`*_{}[\]()#+\-.!|~<>$&])/g, "$1").trim();
@@ -15200,7 +15226,20 @@ function folderFileCaption(name) {
15200
15226
  function isMarkedCaption(label, name) {
15201
15227
  return unescapeCaption(label) === folderFileCaption(name);
15202
15228
  }
15203
- var FENCE_RE = /^(`{3,}|~{3,})/;
15229
+ function uploadedFileOfBlock(block) {
15230
+ const media = block[block.type];
15231
+ const url = media?.type === "file" ? media.file?.url : void 0;
15232
+ if (!url) return null;
15233
+ return { url, caption: (media?.caption ?? []).map((part) => part.plain_text ?? "").join("") };
15234
+ }
15235
+ function isFolderFileBlock(block, knownIds) {
15236
+ const file = uploadedFileOfBlock(block);
15237
+ if (!file) return false;
15238
+ const name = uploadedFileNameFromUrl(file.url);
15239
+ const fileId = notionFileIdOf(file.url);
15240
+ return fileId !== null && knownIds.has(fileId) || name !== null && file.caption === folderFileCaption(name);
15241
+ }
15242
+ var FENCE_RE = /^\s*(`{3,}|~{3,})/;
15204
15243
  function folderFilesInNote(markdown, options = {}) {
15205
15244
  const all = markdown.split("\n");
15206
15245
  const drop = /* @__PURE__ */ new Set();
@@ -15214,7 +15253,7 @@ function folderFilesInNote(markdown, options = {}) {
15214
15253
  return;
15215
15254
  }
15216
15255
  if (fence !== null) return;
15217
- const full = line.trimEnd();
15256
+ const full = line.trim();
15218
15257
  const [span, ...more] = notionMediaSpans(full);
15219
15258
  if (!span || more.length > 0 || span.full !== full || !/^https?:\/\//.test(span.url)) return;
15220
15259
  const name = uploadedFileNameFromUrl(span.url);
@@ -15234,6 +15273,26 @@ function folderFilesInNote(markdown, options = {}) {
15234
15273
  lines
15235
15274
  };
15236
15275
  }
15276
+ function noteFolderFilesOfPage(stateDb, pageId, markdown, embedded) {
15277
+ const ids = new Set(folderFileIdsOf(stateDb, pageId).values());
15278
+ const media = recallUploadedMedia(stateDb, pageId);
15279
+ const found2 = folderFilesInNote(markdown, {
15280
+ knownIds: ids,
15281
+ exclude: (file) => {
15282
+ const fileId = notionFileIdOf(file.url);
15283
+ return embedded?.has(file.name) === true || fileId !== null && media.has(fileId);
15284
+ }
15285
+ });
15286
+ const rest = found2?.markdown ?? markdown;
15287
+ return {
15288
+ found: found2,
15289
+ holdsAll: ids.size === 0 || !notionMediaSpans(rest).some((span) => ids.has(notionFileIdOf(span.url) ?? ""))
15290
+ };
15291
+ }
15292
+ function rootFilesPageOf(config) {
15293
+ if (isDatabaseMode(config) || !config.notion.rootPageId) return null;
15294
+ return normalizeNotionId(config.notion.rootPageId);
15295
+ }
15237
15296
  var WIKI_EMBED_RE = /!\[\[([^\]|#^\n]+)/g;
15238
15297
  var MD_EMBED_RE = /!\[[^\]\n]*\]\(<?([^)>\s]+)/g;
15239
15298
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
@@ -15306,6 +15365,9 @@ function rememberFolderFileStamp(stateDb, pageId, localPath, stamp) {
15306
15365
  }
15307
15366
 
15308
15367
  // src/sync/file-handler.ts
15368
+ function flattenBlockTree2(blocks) {
15369
+ return blocks.flatMap((block) => [block, ...flattenBlockTree2(block.children ?? [])]);
15370
+ }
15309
15371
  function isHiddenPath(path) {
15310
15372
  return path.split("/").some((part) => part.startsWith("."));
15311
15373
  }
@@ -15317,7 +15379,10 @@ function filesFolderOfNote(record) {
15317
15379
  if (record.fileType === "folder-note" && isFolderNotePath(record.obsidianPath)) {
15318
15380
  return getFolderPath(record.obsidianPath);
15319
15381
  }
15320
- return record.fileType === "file" ? stripMarkdownExtension(record.obsidianPath) : null;
15382
+ return isNamedNote(record) ? stripMarkdownExtension(record.obsidianPath) : null;
15383
+ }
15384
+ function isNamedNote(record) {
15385
+ return record.fileType === "file" || record.fileType === "db-row";
15321
15386
  }
15322
15387
  var LOGGED_NAMES = 5;
15323
15388
  var FileHandler = class {
@@ -15330,6 +15395,7 @@ var FileHandler = class {
15330
15395
  this.downloadTimeoutMs = options?.downloadTimeoutMs ?? DEFAULT_CONFIG.advanced.mediaDownloadTimeoutMs;
15331
15396
  this.itemTimeoutMs = options?.itemTimeoutMs ?? DEFAULT_CONFIG.advanced.itemTimeoutMs;
15332
15397
  this.deleteSync = options?.deleteSync ?? false;
15398
+ this.rootPageId = options?.rootPageId ?? null;
15333
15399
  this.attachmentsFolder = (options?.attachmentsFolder ?? DEFAULT_CONFIG.paths.attachments).replace(/^\/+|\/+$/g, "");
15334
15400
  }
15335
15401
  vaultFs;
@@ -15341,6 +15407,7 @@ var FileHandler = class {
15341
15407
  itemTimeoutMs;
15342
15408
  attachmentsFolder;
15343
15409
  deleteSync;
15410
+ rootPageId;
15344
15411
  get fetchFn() {
15345
15412
  return this.customFetch ?? globalThis.fetch;
15346
15413
  }
@@ -15420,19 +15487,22 @@ var FileHandler = class {
15420
15487
  }
15421
15488
  /**
15422
15489
  * 폴더 첨부를 둘 Notion 페이지 — 그 폴더의 Notion 자리({@link folderContainer} 와 같은 차례). 폴더 노트(`X/X.md`)나 노트
15423
- * 이름의 폴더(`X.md` 옆 `X/`)면 그 노트의 페이지 끝(ADR-056), 아니면 push 가 만든 폴더 페이지. 맨 위는 루트 기록이 있을 때만.
15424
- * 행 · DB 폴더는 둘 곳이 없다 — 행의 본문은 행의 것이다.
15490
+ * 이름의 폴더(`X.md` 옆 `X/`)면 그 노트의 페이지 끝(ADR-056), 아니면 push 가 만든 폴더 페이지. 맨 위는 루트 페이지 끝.
15491
+ * 행 이름의 폴더(`DB/행/`)는 그 행의 페이지 끝. DB 폴더는 둘 곳이 없다 — Notion 의 DB 는 본문이 없다.
15425
15492
  */
15426
15493
  folderPageOf(folder) {
15427
- if (folder === "") return this.stateDb.getByPath("__root__")?.notionPageId ?? null;
15428
15494
  return this.notePageOf(folder) ?? this.folderRecordPageOf(folder);
15429
15495
  }
15430
- /** 이 폴더를 품은 노트의 페이지 — 폴더 노트 · 노트 이름의 폴더. 없거나 행이면 null. */
15496
+ /**
15497
+ * 이 폴더를 품은 노트의 페이지 — 폴더 노트 · 노트(행 포함) 이름의 폴더 · 볼트 맨 위는 루트 페이지. 본문이 이 도구의 것이
15498
+ * 아니거나(루트) 노트의 것이라, 적어 둔 폴더 첨부만 제 것으로 본다. 없으면 null.
15499
+ */
15431
15500
  notePageOf(folder) {
15501
+ if (folder === "") return this.rootPageId;
15432
15502
  const note = this.stateDb.getByPath(folderNoteOf(folder));
15433
15503
  if (note?.notionPageId && note.fileType === "folder-note") return note.notionPageId;
15434
15504
  const named = this.stateDb.getByPath(`${folder}.md`);
15435
- return named?.notionPageId && named.fileType === "file" ? named.notionPageId : null;
15505
+ return named?.notionPageId && isNamedNote(named) ? named.notionPageId : null;
15436
15506
  }
15437
15507
  folderRecordPageOf(folder) {
15438
15508
  const record = this.stateDb.getByPath(folder);
@@ -15538,21 +15608,26 @@ var FileHandler = class {
15538
15608
  */
15539
15609
  async removeDeletedFolderFiles(noteWrites) {
15540
15610
  const removed = [];
15611
+ const pages = [];
15541
15612
  for (const record of this.stateDb.getAll()) {
15542
15613
  if (!record.notionPageId) continue;
15543
15614
  const folder = isFolderRecord(record) ? record.obsidianPath : filesFolderOfNote(record);
15544
- if (folder === null) continue;
15545
- const ids = folderFileIdsOf(this.stateDb, record.notionPageId);
15546
- if (!isFolderRecord(record) && ids.size === 0) continue;
15615
+ if (folder !== null) {
15616
+ pages.push({ pageId: record.notionPageId, folder, onNote: !isFolderRecord(record) });
15617
+ }
15618
+ }
15619
+ if (this.rootPageId) pages.push({ pageId: this.rootPageId, folder: "", onNote: true });
15620
+ for (const { pageId, folder, onNote } of pages) {
15621
+ const ids = folderFileIdsOf(this.stateDb, pageId);
15622
+ if (onNote && ids.size === 0) continue;
15547
15623
  const gone = [];
15548
- for (const entry of this.stateDb.getFilesByPageId(record.notionPageId)) {
15624
+ for (const entry of this.stateDb.getFilesByPageId(pageId)) {
15549
15625
  if (getFolderPath(entry.localPath) !== folder) continue;
15550
- if (!isFolderRecord(record) && !ids.has(entry.localPath)) continue;
15626
+ if (onNote && !ids.has(entry.localPath)) continue;
15551
15627
  if (!await this.vaultFs.exists(entry.localPath)) gone.push(entry);
15552
15628
  }
15553
15629
  if (gone.length === 0) continue;
15554
- const pageId = record.notionPageId;
15555
- await this.writingNotePage(!isFolderRecord(record), pageId, noteWrites, async () => {
15630
+ await this.writingNotePage(onNote, pageId, noteWrites, async () => {
15556
15631
  for (const entry of gone) {
15557
15632
  try {
15558
15633
  const name = entry.localPath.slice(entry.localPath.lastIndexOf("/") + 1);
@@ -15587,7 +15662,7 @@ var FileHandler = class {
15587
15662
  /** 폴더 첨부를 둘 페이지 — 노트 없는 폴더면 마련한다. 둘 곳이 없으면 그 파일들을 `stranded` 에 담고 null. */
15588
15663
  async provideFolderPage(folder, folders, items, stranded) {
15589
15664
  let pageId = this.folderPageOf(folder);
15590
- let reason = folder === "" ? "\uBCFC\uD2B8 \uB9E8 \uC704" : "DB \uD3F4\uB354 \xB7 \uC544\uC9C1 \uC62C\uB9AC\uC9C0 \uC54A\uC740 \uD3F4\uB354 \uB178\uD2B8";
15665
+ let reason = folder === "" ? "\uBCFC\uD2B8 \uB9E8 \uC704 \u2014 DB \uBAA8\uB4DC\uC758 \uB8E8\uD2B8\uB294 DB \uB77C \uBCF8\uBB38\uC774 \uC5C6\uB2E4" : "DB \uD3F4\uB354 \xB7 \uC544\uC9C1 \uC62C\uB9AC\uC9C0 \uC54A\uC740 \uD3F4\uB354 \uB178\uD2B8";
15591
15666
  if (!pageId && folder !== "" && folders && !this.stateDb.getByPath(folder)) {
15592
15667
  try {
15593
15668
  await folders.ensureFolderPage(folder);
@@ -15838,22 +15913,23 @@ var FileHandler = class {
15838
15913
  */
15839
15914
  async removeOlderBlocks(pageId, localPath, name, keep) {
15840
15915
  const record = this.stateDb.getByNotionId(pageId);
15841
- const onNote = record !== null && !isFolderRecord(record);
15916
+ const onNote = record !== null && !isFolderRecord(record) || this.rootPageId !== null && notionIdsEqual(pageId, this.rootPageId);
15842
15917
  const fileId = folderFileIdsOf(this.stateDb, pageId).get(localPath);
15843
- for (const block of await this.notionClient.fetchAllChildren(pageId)) {
15844
- if (block.id === keep) continue;
15845
- const media = block[block.type];
15846
- const url = media?.type === "file" ? media.file?.url : void 0;
15847
- if (!url || uploadedFileNameFromUrl(url) !== name) continue;
15918
+ const isOlder = (block) => {
15919
+ if (block.id === keep) return false;
15920
+ const media = uploadedFileOfBlock(block);
15921
+ if (!media || uploadedFileNameFromUrl(media.url) !== name) return false;
15848
15922
  if (keep === void 0 && fileId !== void 0) {
15849
- if (notionFileIdOf(url) !== fileId) continue;
15850
- } else if (onNote) {
15851
- const caption = (media?.caption ?? []).map((part) => part.plain_text ?? "").join("");
15852
- const ours = fileId !== void 0 && notionFileIdOf(url) === fileId || caption === folderFileCaption(name);
15853
- if (!ours) continue;
15923
+ return notionFileIdOf(media.url) === fileId;
15854
15924
  }
15855
- await this.notionClient.deleteBlock(block.id);
15925
+ if (!onNote) return true;
15926
+ return fileId !== void 0 && notionFileIdOf(media.url) === fileId || media.caption === folderFileCaption(name);
15927
+ };
15928
+ let older = (await this.notionClient.fetchAllChildren(pageId)).filter(isOlder);
15929
+ if (older.length === 0 && onNote) {
15930
+ older = flattenBlockTree2(await this.notionClient.getBlockTree(pageId)).filter(isOlder);
15856
15931
  }
15932
+ for (const block of older) await this.notionClient.deleteBlock(block.id);
15857
15933
  }
15858
15934
  /**
15859
15935
  * 폴더 첨부의 파일 블록 — 캡션에 표시를 단 파일 이름을 적는다({@link folderFileCaption}). 받는 쪽이 그 표시로 폴더 첨부를
@@ -16889,21 +16965,6 @@ function loadLinkedDbMap(stateDb) {
16889
16965
  }
16890
16966
  }
16891
16967
 
16892
- // src/sync/parent-mode.ts
16893
- function isDatabaseMode(config) {
16894
- return config.notion.parentMode === "database" && !!config.notion.databaseId;
16895
- }
16896
- function rowDatabaseOf(config, record) {
16897
- if (record.fileType === "db-row") {
16898
- if (!record.notionParentId) {
16899
- throw new Error(`DB \uD589\uC778\uB370 \uC18C\uC18D DB \uB97C \uC54C \uC218 \uC5C6\uC74C: ${record.obsidianPath}`);
16900
- }
16901
- return record.notionParentId;
16902
- }
16903
- if (isDatabaseMode(config)) return config.notion.databaseId;
16904
- return null;
16905
- }
16906
-
16907
16968
  // src/sync/remote-scan.ts
16908
16969
  var LAST_FULL_PULL_META_KEY = "last_full_pull_at";
16909
16970
  var LAST_PULL_META_KEY = "last_pull_at";
@@ -19874,6 +19935,7 @@ function emptyDatabaseSyncResult() {
19874
19935
  }
19875
19936
  var DatabaseSyncer = class {
19876
19937
  constructor(config, stateDb, notionClient, vaultFs, pipeline, imageHandler, observation = () => NO_OBSERVATION, noteRowBody = () => {
19938
+ }, receiveRowFiles = async () => {
19877
19939
  }) {
19878
19940
  this.config = config;
19879
19941
  this.stateDb = stateDb;
@@ -19883,6 +19945,7 @@ var DatabaseSyncer = class {
19883
19945
  this.imageHandler = imageHandler;
19884
19946
  this.observation = observation;
19885
19947
  this.noteRowBody = noteRowBody;
19948
+ this.receiveRowFiles = receiveRowFiles;
19886
19949
  this.baseFileInfo = new DbBaseFiles(stateDb);
19887
19950
  this.journal = new UpdateJournal(stateDb);
19888
19951
  this.sidecarBases = new SidecarBases(stateDb);
@@ -19907,6 +19970,7 @@ var DatabaseSyncer = class {
19907
19970
  imageHandler;
19908
19971
  observation;
19909
19972
  noteRowBody;
19973
+ receiveRowFiles;
19910
19974
  propertyMapper;
19911
19975
  decorationsCache;
19912
19976
  /**
@@ -20989,7 +21053,18 @@ var DatabaseSyncer = class {
20989
21053
  this.noteRowBody(page.id, mdResult.markdown);
20990
21054
  const bodyFingerprint = remoteBodyFingerprint(mdResult.markdown);
20991
21055
  const exportCompact = isCompactExport(mdResult.markdown);
20992
- let markdown = await notionBodyToObsidian(this.notionClient, mdResult.markdown, page.id);
21056
+ const localContent = await readLocalNote(this.vaultFs, options?.localPath ?? filePath);
21057
+ const noteFiles = noteFolderFilesOfPage(
21058
+ this.stateDb,
21059
+ page.id,
21060
+ mdResult.markdown,
21061
+ embeddedNamesOf(localContent === void 0 ? [] : [localContent])
21062
+ );
21063
+ let markdown = await notionBodyToObsidian(
21064
+ this.notionClient,
21065
+ noteFiles.found?.markdown ?? mdResult.markdown,
21066
+ page.id
21067
+ );
20993
21068
  markdown = await this.imageHandler.restoreUploadedMedia(
20994
21069
  markdown,
20995
21070
  page.id,
@@ -21023,7 +21098,7 @@ var DatabaseSyncer = class {
21023
21098
  {
21024
21099
  properties,
21025
21100
  notionExportCompact: exportCompact,
21026
- localContent: await readLocalNote(this.vaultFs, options?.localPath ?? filePath),
21101
+ localContent,
21027
21102
  // 이 렌더러가 적는 키 — 속성과 제목 · 커버 · 아이콘. 나머지는 로컬 키다(F-08).
21028
21103
  notionKeys: [...this.propertyMapper.ownedKeys(rawProperties), "title", ...DECORATION_KEYS]
21029
21104
  }
@@ -21033,7 +21108,11 @@ var DatabaseSyncer = class {
21033
21108
  title,
21034
21109
  properties,
21035
21110
  bodyFingerprint,
21036
- propertiesBlock: hasPropertiesBlock(mdResult.markdown)
21111
+ propertiesBlock: hasPropertiesBlock(mdResult.markdown),
21112
+ folderFiles: {
21113
+ files: noteFiles.found?.files ?? [],
21114
+ complete: !mdResult.truncated && noteFiles.holdsAll
21115
+ }
21037
21116
  };
21038
21117
  }
21039
21118
  /**
@@ -21330,8 +21409,19 @@ var DatabaseSyncer = class {
21330
21409
  */
21331
21410
  adoptedChildrenOf(record, rowPath) {
21332
21411
  const from = record.fileType === "folder-note" ? parentFolderOf(record.obsidianPath) : stripMarkdownExtension(record.obsidianPath);
21333
- const hasChildren = this.stateDb.getAll().some((r) => r.id !== record.id && within(r.obsidianPath, from));
21334
- return hasChildren ? { from, to: stripMarkdownExtension(rowPath) } : null;
21412
+ return this.holdsChildren(record, from) ? { from, to: stripMarkdownExtension(rowPath) } : null;
21413
+ }
21414
+ /**
21415
+ * 이 노트 · 행 옆의 폴더(`from`)가 그것의 것인가 — 하위 페이지가 있거나, 그 페이지에 올린 폴더 첨부(ADR-056)가 든 폴더.
21416
+ * 예전에는 하위 페이지만 보아 행의 이름을 따라 옮길 때 폴더 첨부만 든 폴더가 옛 이름에 남았다.
21417
+ */
21418
+ holdsChildren(record, from) {
21419
+ if (this.stateDb.getAll().some((r) => r.id !== record.id && within(r.obsidianPath, from))) {
21420
+ return true;
21421
+ }
21422
+ return record.notionPageId !== null && [...folderFileIdsOf(this.stateDb, record.notionPageId).keys()].some(
21423
+ (path) => within(path, from)
21424
+ );
21335
21425
  }
21336
21426
  /**
21337
21427
  * Notion 에서 바꾼 제목으로 행 이름을 바꿀 자리(`sync.renameOnTitleChange`) — 바꿀 것이 없으면 null.
@@ -21353,14 +21443,32 @@ var DatabaseSyncer = class {
21353
21443
  );
21354
21444
  if (to === recordPath) return null;
21355
21445
  const from = stripMarkdownExtension(recordPath);
21356
- if (!this.stateDb.getAll().some((r) => within(r.obsidianPath, from))) {
21446
+ if (!this.holdsChildren(record, from)) {
21357
21447
  return { to, children: null };
21358
21448
  }
21359
21449
  const children = { from, to: stripMarkdownExtension(to) };
21360
21450
  const blocked = await this.relocator.blocked(children);
21361
21451
  return blocked ? { reason: `\uD558\uC704 \uD398\uC774\uC9C0 \uD3F4\uB354 \u2014 ${blocked}` } : { to, children };
21362
21452
  }
21453
+ /**
21454
+ * 행 하나를 받고 그 행 페이지의 폴더 첨부를 행 이름의 폴더에 받는다(ADR-056) — 노트가 충돌로 남거나 로컬을 지켜도 파일은
21455
+ * 제 규칙대로 받는다(페이지와 같다). 행을 옮겼으면 옮긴 자리로.
21456
+ */
21363
21457
  async pullDatabasePage(page, dbConfig) {
21458
+ const rendered = {};
21459
+ const outcome = await this.pullDatabaseRow(page, dbConfig, rendered);
21460
+ const path = outcome.path ?? this.stateDb.getByNotionId(page.id)?.obsidianPath;
21461
+ if (rendered.folderFiles && path) {
21462
+ await this.receiveRowFiles(
21463
+ stripMarkdownExtension(path),
21464
+ page.id,
21465
+ rendered.folderFiles.files,
21466
+ rendered.folderFiles.complete
21467
+ );
21468
+ }
21469
+ return outcome;
21470
+ }
21471
+ async pullDatabaseRow(page, dbConfig, seen) {
21364
21472
  const safeName = sanitizeFileName(this.notionClient.extractTitle(page));
21365
21473
  const existingRecord = this.stateDb.getByNotionId(page.id);
21366
21474
  const recordPath = existingRecord?.obsidianPath;
@@ -21414,6 +21522,7 @@ var DatabaseSyncer = class {
21414
21522
  localPath: recordPath ?? filePath,
21415
21523
  ...local !== void 0 ? { adoptFrom: local } : {}
21416
21524
  });
21525
+ seen.folderFiles = rendered.folderFiles;
21417
21526
  let relinked = false;
21418
21527
  if (adopt) {
21419
21528
  const outcome = local === void 0 ? null : await this.relinkExistingRow(
@@ -21436,6 +21545,7 @@ var DatabaseSyncer = class {
21436
21545
  await this.rowPathOwners(dbConfig.localFolder, safeName, page.id)
21437
21546
  );
21438
21547
  rendered = await this.renderRow(page, filePath, { localPath: filePath });
21548
+ seen.folderFiles = rendered.folderFiles;
21439
21549
  }
21440
21550
  }
21441
21551
  const { title, properties, bodyFingerprint } = rendered;
@@ -23690,7 +23800,6 @@ function dropEmbeddedBaseMarkers(content, basePaths) {
23690
23800
 
23691
23801
  // src/sync/database-discovery.ts
23692
23802
  var DISCOVERY_RETRY_META_KEY = "discovery_retry";
23693
- var PREVIEW_BODY_READ_BUDGET = 100;
23694
23803
  function parseDiscoveryRetry(raw) {
23695
23804
  if (!raw) return { parents: [], dbs: [] };
23696
23805
  try {
@@ -23908,7 +24017,8 @@ var DatabaseDiscovery = class {
23908
24017
  * `markScanned` 일 때 적는다.
23909
24018
  *
23910
24019
  * @param failed 읽지 못한 페이지 · DB — 이유와 함께 싣는다.
23911
- * @param options.notePathOf 페이지 → 볼트 경로. 새 DB 폴더를 부모 노트 옆에 둔다 — dry-run 은 다시 이을 노트의 자리를 준다.
24020
+ * @param options.notePathOf 페이지 → 볼트 경로. 새 DB 폴더를 부모 노트 옆에 둔다 — dry-run 은 다시 이을 노트 · 새로 받을 페이지의
24021
+ * 자리를 준다.
23912
24022
  */
23913
24023
  async openRegistry(failed, options) {
23914
24024
  const { forceRediscovery, signal } = options;
@@ -24090,18 +24200,22 @@ var DatabaseDiscovery = class {
24090
24200
  }
24091
24201
  /**
24092
24202
  * dry-run — 새로 받을 페이지의 본문을 읽어 그 안의 DB 를 찾는다({@link plannedNewDatabases} 가 그 행을 센다). 실제 pull 은
24093
- * 받으며 찾는다. 예전에는 읽지 않고 «미리 세지 않았습니다» 만 알렸다. {@link PREVIEW_BODY_READ_BUDGET} 개까지만 읽는다 —
24094
- * 넘거나 읽지 못한(다 받지 못한) 페이지는 {@link bodyRead} 가 아니어서 알림으로 남는다.
24203
+ * 받으며 찾는다 — 같은 길로 읽는다: Markdown API(잘린 본문은 채운 것 · 권한 없는 자리표시는 그대로), 실패하거나 블록 방식이면
24204
+ * 블록 트리. 예전에는 100개까지만 읽고 넘는 페이지 안의 DB 를 «미리 세지 않았습니다» 로만 알렸다 — 미리 보기는 사용자가
24205
+ * 부르고 멈출 수 있어(`signal`) 모두 읽는다. 둘 다 읽지 못한 페이지만 {@link bodyRead} 가 아니어서 알림으로 남는다.
24095
24206
  */
24096
24207
  async readNewPageBodies(pageIds, signal) {
24097
24208
  if (isDatabaseMode(this.config)) return;
24098
24209
  const unread = pageIds.filter((pageId) => !this.bodyRead(pageId));
24099
- for (const pageId of unread.slice(0, PREVIEW_BODY_READ_BUDGET)) {
24210
+ if (unread.length > 0) {
24211
+ getLogger().info(
24212
+ `[Im-Nobsidian] \uBBF8\uB9AC \uBCF4\uAE30: \uC0C8 \uD398\uC774\uC9C0 ${unread.length}\uAC1C\uC758 \uBCF8\uBB38\uC5D0\uC11C DB \uB97C \uCC3E\uC74C`
24213
+ );
24214
+ }
24215
+ for (const pageId of unread) {
24100
24216
  if (signal?.aborted) return;
24101
24217
  try {
24102
- const result = await this.notionClient.getPageMarkdown(pageId);
24103
- if (result.truncated || (result.unknown_block_ids ?? []).length > 0) continue;
24104
- this.collectInlineDbRefs(pageId, result.markdown);
24218
+ this.collectInlineDbRefs(pageId, await this.previewBodyOf(pageId));
24105
24219
  } catch (error) {
24106
24220
  getLogger().debug(
24107
24221
  `[Im-Nobsidian] \uBBF8\uB9AC \uBCF4\uAE30: \uC0C8 \uD398\uC774\uC9C0 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD574 \uC548\uC758 DB \uB294 \uBC1B\uC73C\uBA70 \uCC3E\uC74C (${pageId}): ${error instanceof Error ? error.message : String(error)}`
@@ -24109,6 +24223,16 @@ var DatabaseDiscovery = class {
24109
24223
  }
24110
24224
  }
24111
24225
  }
24226
+ /** 미리 보기가 읽는 새 페이지 본문 — 실제 pull 의 `PagePuller.fetchPageMarkdown` 과 같은 차례. */
24227
+ async previewBodyOf(pageId) {
24228
+ if (this.config.conversion.preferMarkdownApi !== false) {
24229
+ try {
24230
+ return (await this.notionClient.getPageMarkdown(pageId)).markdown;
24231
+ } catch {
24232
+ }
24233
+ }
24234
+ return blocksToNfm(await this.notionClient.getBlockTree(pageId), { pageId });
24235
+ }
24112
24236
  /** 이번 pull 에서 본문을 받은 페이지인가 — 그 본문의 `<database>` 로 새 DB 를 찾았다. */
24113
24237
  bodyRead(pageId) {
24114
24238
  return this._bodyReadPages.has(normalizeNotionId(pageId));
@@ -24191,7 +24315,7 @@ var DatabaseDiscovery = class {
24191
24315
  getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uC124\uC815 \uC0DD\uC131 \uC2E4\uD328:`, error);
24192
24316
  return { kind: "error", error: error instanceof Error ? error.message : String(error) };
24193
24317
  }
24194
- const obsPath = parentPageId ? notePathOf(parentPageId) : null;
24318
+ const obsPath = parentPageId ? await notePathOf(parentPageId) : null;
24195
24319
  let parentFolder = "";
24196
24320
  if (obsPath) {
24197
24321
  if (isMarkdownPath(obsPath)) {
@@ -27598,7 +27722,8 @@ var PagePusher = class {
27598
27722
  * 실패로 알린다. 예전처럼 조용히 건너뛰면 동기화됨으로 기록돼 편집이 영영 가지 않는다.
27599
27723
  *
27600
27724
  * 바뀌지 않은 미디어는 다시 올리지 않고 Notion 의 그 블록을 둔다({@link ImageHandler.keepUploadedMedia}).
27601
- * 블록 방식은 블록을 모두 지우므로 둘 수 없다 — 예전처럼 모두 다시 올린다.
27725
+ * 블록 방식은 노트의 미디어를 둘 수 없다 — 예전처럼 모두 다시 올린다. 페이지 끝의 폴더 첨부(ADR-056)는 노트 밖의 것이라
27726
+ * 블록째 두고 새 본문을 그 앞(맨 앞)에 넣는다.
27602
27727
  *
27603
27728
  * @returns 보낸 본문 · 그 지문 · 그대로 둔 미디어({@link PushedBody})
27604
27729
  */
@@ -27610,9 +27735,9 @@ var PagePusher = class {
27610
27735
  deferred.nfm,
27611
27736
  notePath
27612
27737
  );
27613
- const folderFiles = await this.folderFileLines(pageId);
27614
- const markdown = folderFiles ? `${body}
27615
- ${folderFiles}` : body;
27738
+ const folderFiles2 = await this.folderFileLines(pageId);
27739
+ const markdown = folderFiles2 ? `${body}
27740
+ ${folderFiles2}` : body;
27616
27741
  const written = await replacePageBody(this.notionClient, pageId, markdown, {
27617
27742
  databaseIdsOfBase: (basePath) => this.databaseIdsOfBase(basePath),
27618
27743
  unknownTagFallbacks: unknownTagFallbacks(markdownContent)
@@ -27632,14 +27757,19 @@ ${folderFiles}` : body;
27632
27757
  "\uC790\uC2DD \uD398\uC774\uC9C0 \xB7 DB \uAC00 \uC788\uB294 \uD398\uC774\uC9C0\uB294 \uBE14\uB85D \uBC29\uC2DD\uC73C\uB85C \uBCF8\uBB38\uC744 \uBCF4\uB0B4\uBA74 \uC790\uC2DD\uAE4C\uC9C0 \uC9C0\uC6CC\uC838 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C \u2014 \uC124\uC815 conversion.preferMarkdownApi \uB97C \uAE30\uBCF8\uAC12(true)\uC73C\uB85C \uB450\uACE0 \uB2E4\uC2DC push \uD558\uC138\uC694"
27633
27758
  );
27634
27759
  }
27635
- this.forgetFolderFilesForBlockMode(pageId, notePath);
27760
+ const folderFiles = this.folderFileBlocks(existingBlocks, pageId, notePath);
27636
27761
  const { nfm, blocks } = blockModeBody(markdownContent);
27637
27762
  if (blocks.length > 0) {
27638
- await this.notionClient.appendBlockTree(pageId, blocks);
27763
+ await this.notionClient.appendBlockTree(
27764
+ pageId,
27765
+ blocks,
27766
+ folderFiles.size > 0 ? { atStart: true } : void 0
27767
+ );
27639
27768
  }
27640
27769
  const deleteSema = new Sema(this.config.advanced.concurrency);
27641
27770
  await Promise.all(
27642
27771
  existingBlocks.map(async (block) => {
27772
+ if (folderFiles.has(block.id)) return;
27643
27773
  await deleteSema.acquire();
27644
27774
  try {
27645
27775
  await this.notionClient.deleteBlock(block.id);
@@ -27662,19 +27792,28 @@ ${folderFiles}` : body;
27662
27792
  return (folderFilesInNote(remote, { knownIds: ids })?.lines ?? []).join("\n");
27663
27793
  }
27664
27794
  /**
27665
- * 블록 방식은 본문 블록을 모두 지우고 새로 붙인다 — 폴더 첨부 블록을 남기면 새 본문이 그 뒤에 붙는다.
27666
- * 함께 지우고 기록을 놓는다 — 이어지는 첨부 push 가 볼트의 파일을 다시 끝에 올린다.
27667
- */
27668
- forgetFolderFilesForBlockMode(pageId, notePath) {
27669
- const ids = folderFileIdsOf(this.stateDb, pageId);
27670
- if (ids.size === 0) return;
27671
- for (const localPath of ids.keys()) {
27795
+ * 블록 방식으로 바꿀 본문에서 남길 폴더 첨부 블록(ADR-056) — 맨 위 블록 가운데 {@link isFolderFileBlock}. 예전에는 함께 지우고
27796
+ * 기록을 놓아 노트를 고칠 때마다 폴더 첨부를 다시 올렸다.
27797
+ *
27798
+ * 맨 위에 없는 폴더 첨부(Notion 에서 토글 안으로 옮긴 것)는 그 토글과 함께 지워진다 — 기록을 놓아 이어지는 첨부 push 가
27799
+ * 페이지 끝에 다시 올리게 한다. 두면 볼트의 기록만 남아 다음 pull 이 Notion 에서 지운 파일로 본다.
27800
+ */
27801
+ folderFileBlocks(blocks, pageId, notePath) {
27802
+ const recorded = folderFileIdsOf(this.stateDb, pageId);
27803
+ const ids = new Set(recorded.values());
27804
+ const kept = blocks.filter((block) => isFolderFileBlock(block, ids));
27805
+ const keptIds = new Set(kept.map((block) => notionFileIdOf(fileUrlOfBlock(block) ?? "")));
27806
+ const lost = [...recorded].filter(([, fileId]) => !keptIds.has(fileId));
27807
+ for (const [localPath] of lost) {
27672
27808
  this.stateDb.deleteFileRegistry(localPath);
27673
27809
  forgetFolderFileId(this.stateDb, pageId, localPath);
27674
27810
  }
27675
- getLogger().info(
27676
- `[Im-Nobsidian] \uBE14\uB85D \uBC29\uC2DD\uC73C\uB85C \uBCF8\uBB38\uC744 \uBC14\uAFD4 \uD3F4\uB354 \uCCA8\uBD80 ${ids.size}\uAC1C\uB97C \uB2E4\uC2DC \uC62C\uB9BC (${notePath})`
27677
- );
27811
+ if (lost.length > 0) {
27812
+ getLogger().info(
27813
+ `[Im-Nobsidian] \uBE14\uB85D \uBC29\uC2DD\uC73C\uB85C \uBCF8\uBB38\uC744 \uBC14\uAFB8\uBA70 \uB9E8 \uC704\uC5D0 \uC5C6\uB358 \uD3F4\uB354 \uCCA8\uBD80 ${lost.length}\uAC1C\uB97C \uB2E4\uC2DC \uC62C\uB9BC (${notePath})`
27814
+ );
27815
+ }
27816
+ return new Set(kept.map((block) => block.id));
27678
27817
  }
27679
27818
  /**
27680
27819
  * 자리표시로 보낸 코드를 채우고(S-22) 동기화 블록 참조를 붙인다({@link deferSyncedReferences}) — 본문을
@@ -27914,6 +28053,17 @@ var PagePuller = class {
27914
28053
  sanitizeFileName(this.notionClient.extractTitle(page))
27915
28054
  );
27916
28055
  }
28056
+ /**
28057
+ * dry-run — 새 페이지를 실제 pull 이 받을 자리(부모의 자리 + 제목). 그 안에서 찾은 DB 의 폴더를 실제처럼 그 옆에 둔다 — 예전에는
28058
+ * 자리를 몰라 `databases/` 아래로 보였다. 같은 이름이 있으면 실제 pull 은 접미사를 붙이지만 여기서는 붙이지 않는다(선점하지
28059
+ * 않는다). 볼트도 상태 DB 도 바꾸지 않는다.
28060
+ */
28061
+ async plannedNotePath(pageId) {
28062
+ const page = this.detector.discoveredPage(pageId) ?? await this.notionClient.getPage(pageId);
28063
+ const parent = await this.resolveParentPath(page);
28064
+ const name = `${sanitizeFileName(this.notionClient.extractTitle(page))}.md`;
28065
+ return parent ? `${parent}/${name}` : name;
28066
+ }
27917
28067
  /**
27918
28068
  * dry-run — 실제 pull({@link createPulledPage})이 이 새 페이지를 새로 받지 않고 상태 기록에 없던 노트 · 폴더와 다시 잇는가(P7).
27919
28069
  * 다시 이을 노트(`note` — 충돌로 남길 것이면 `conflict`) · 이미 있는 폴더(`folder` — 폴더로만 받는 페이지), 아니면 null.
@@ -28489,13 +28639,13 @@ var PagePuller = class {
28489
28639
  * 받았으면 null — 지문은 Markdown API 의 본문으로만 견준다.
28490
28640
  */
28491
28641
  async fetchPageMarkdown(pageId, options = {}) {
28492
- const noteFilesOf = (markdown) => this.noteFolderFilesOf(pageId, markdown, options);
28642
+ const noteFilesOf = (markdown) => noteFolderFilesOfPage(this.stateDb, pageId, markdown, options.embedded);
28493
28643
  if (this.config.conversion.preferMarkdownApi !== false) {
28494
28644
  try {
28495
28645
  const result = await this.notionClient.getPageMarkdown(pageId);
28496
28646
  this.discovery.collectInlineDbRefs(pageId, result.markdown);
28497
28647
  this.detector.noteBodyChildren(pageId, result.markdown);
28498
- const found2 = noteFilesOf(result.markdown);
28648
+ const { found: found2, holdsAll } = noteFilesOf(result.markdown);
28499
28649
  return {
28500
28650
  content: await this.resolveBlockLinks(
28501
28651
  this.resolveNotionIdWikilinks(
@@ -28515,7 +28665,7 @@ var PagePuller = class {
28515
28665
  hasBody: hasBodyBesidesChildren(result.markdown),
28516
28666
  folderFiles: folderFilesInBody(result.markdown),
28517
28667
  noteFiles: found2?.files ?? [],
28518
- noteFilesComplete: !result.truncated && this.noteFilesHoldAll(pageId, found2?.markdown ?? result.markdown)
28668
+ noteFilesComplete: !result.truncated && holdsAll
28519
28669
  };
28520
28670
  } catch (error) {
28521
28671
  getLogger().warn(
@@ -28526,9 +28676,9 @@ var PagePuller = class {
28526
28676
  let noteFiles = [];
28527
28677
  let noteFilesComplete = true;
28528
28678
  const fallback = await pageBlocksToObsidian(this.notionClient, pageId, (markdown) => {
28529
- const found2 = noteFilesOf(markdown);
28679
+ const { found: found2, holdsAll } = noteFilesOf(markdown);
28530
28680
  noteFiles = found2?.files ?? [];
28531
- noteFilesComplete = this.noteFilesHoldAll(pageId, found2?.markdown ?? markdown);
28681
+ noteFilesComplete = holdsAll;
28532
28682
  return found2?.markdown ?? markdown;
28533
28683
  });
28534
28684
  this.discovery.collectInlineDbRefs(pageId, fallback.markdown);
@@ -28547,24 +28697,39 @@ var PagePuller = class {
28547
28697
  noteFilesComplete
28548
28698
  };
28549
28699
  }
28550
- /** 노트 페이지의 폴더 첨부({@link folderFilesInNote}) — 없으면 null. */
28551
- noteFolderFilesOf(pageId, markdown, options) {
28552
- const media = recallUploadedMedia(this.stateDb, pageId);
28553
- return folderFilesInNote(markdown, {
28554
- knownIds: new Set(folderFileIdsOf(this.stateDb, pageId).values()),
28555
- exclude: (file) => {
28556
- const fileId = notionFileIdOf(file.url);
28557
- return options.embedded?.has(file.name) === true || fileId !== null && media.has(fileId);
28700
+ /**
28701
+ * 볼트 맨 위 파일 — 루트 페이지 끝의 폴더 첨부(「📁 이름」 · 적어 둔 것)를 볼트 맨 위에 받는다({@link rootFilesPageOf}).
28702
+ * 루트의 다른 블록(사용자가 붙인 파일 포함)은 받지 않는다. Markdown API 로 읽지 못하면 맨 위 블록에서 찾되 빠진 파일을
28703
+ * 지운 것으로 보지 않는다 — 토글 안으로 옮긴 것은 보이지 않는다. 읽지 못하면 경고만 남긴다(다음 pull 이 다시 읽는다).
28704
+ */
28705
+ async receiveRootFiles() {
28706
+ const rootId = rootFilesPageOf(this.config);
28707
+ if (!rootId) return;
28708
+ try {
28709
+ let files;
28710
+ let complete2;
28711
+ try {
28712
+ const result = await this.notionClient.getPageMarkdown(rootId);
28713
+ const { found: found2, holdsAll } = noteFolderFilesOfPage(this.stateDb, rootId, result.markdown);
28714
+ files = found2?.files ?? [];
28715
+ complete2 = !result.truncated && holdsAll;
28716
+ } catch {
28717
+ const ids = new Set(folderFileIdsOf(this.stateDb, rootId).values());
28718
+ files = (await this.notionClient.fetchAllChildren(rootId)).flatMap((block) => {
28719
+ const url = isFolderFileBlock(block, ids) ? fileUrlOfBlock(block) : null;
28720
+ const name = url ? uploadedFileNameFromUrl(url) : null;
28721
+ return url && name ? [{ url, name }] : [];
28722
+ });
28723
+ complete2 = false;
28558
28724
  }
28559
- });
28560
- }
28561
- /** 폴더 첨부를 뗀 본문에 적어 둔 폴더 첨부가 남지 않았나 — {@link fetchPageMarkdown} 의 `noteFilesComplete`. */
28562
- noteFilesHoldAll(pageId, rest) {
28563
- const ids = new Set(folderFileIdsOf(this.stateDb, pageId).values());
28564
- if (ids.size === 0) return true;
28565
- return !notionMediaSpans(rest).some((span) => ids.has(notionFileIdOf(span.url) ?? ""));
28725
+ await this.receiveFolderFiles("", rootId, files, complete2);
28726
+ } catch (error) {
28727
+ getLogger().warn(
28728
+ `[Im-Nobsidian] \uBCFC\uD2B8 \uB9E8 \uC704 \uD30C\uC77C\uC744 \uB8E8\uD2B8 \uD398\uC774\uC9C0\uC5D0\uC11C \uBC1B\uC9C0 \uBABB\uD568: ${error instanceof Error ? error.message : String(error)}`
28729
+ );
28730
+ }
28566
28731
  }
28567
- /** 폴더 첨부를 받아 받은 파일 수에 더한다. */
28732
+ /** 폴더 첨부를 받아 받은 파일 수에 더한다 — 행의 폴더 첨부도 여기로 받는다(`DatabaseSyncer`). */
28568
28733
  async receiveFolderFiles(folder, pageId, files, complete2 = false) {
28569
28734
  this._pullFileCount += await this.fileHandler.receiveFolderFiles(
28570
28735
  folder,
@@ -29035,13 +29200,12 @@ var PullPlanner = class {
29035
29200
  const relinkPaths = new Map(
29036
29201
  [...relinks.byPage].map(([pageId, relink]) => [compactNotionId(pageId), relink.path])
29037
29202
  );
29038
- await this.discovery.readNewPageBodies(
29039
- planned.filter((change) => change.type === "created").map((change) => change.pageId),
29040
- options.signal
29041
- );
29203
+ const created = planned.filter((change) => change.type === "created").map((change) => change.pageId);
29204
+ await this.discovery.readNewPageBodies(created, options.signal);
29205
+ const createdIds = new Set(created.map(compactNotionId));
29042
29206
  const newDatabases = await this.discovery.plannedNewDatabases(failed, {
29043
29207
  rescanRoot: this.detector.rootChanged(),
29044
- notePathOf: (pageId) => relinkPaths.get(compactNotionId(pageId)) ?? this.stateDb.getByNotionId(pageId)?.obsidianPath ?? null
29208
+ notePathOf: async (pageId) => relinkPaths.get(compactNotionId(pageId)) ?? this.stateDb.getByNotionId(pageId)?.obsidianPath ?? (createdIds.has(compactNotionId(pageId)) ? await this.puller.plannedNotePath(pageId) : null)
29045
29209
  });
29046
29210
  const rows = [];
29047
29211
  for (const row of await this.planDatabaseRows(
@@ -29996,7 +30160,8 @@ var SyncOrchestrator = class {
29996
30160
  downloadTimeoutMs: config.advanced.mediaDownloadTimeoutMs,
29997
30161
  itemTimeoutMs: config.advanced.itemTimeoutMs,
29998
30162
  attachmentsFolder: config.paths.attachments,
29999
- deleteSync: config.sync.deleteSync
30163
+ deleteSync: config.sync.deleteSync,
30164
+ rootPageId: rootFilesPageOf(config)
30000
30165
  }
30001
30166
  );
30002
30167
  const propertyMapper = PropertyMapper.fromConfig(config);
@@ -30008,7 +30173,8 @@ var SyncOrchestrator = class {
30008
30173
  pipeline,
30009
30174
  imageHandler,
30010
30175
  () => this.observation.context,
30011
- (pageId, rawMarkdown) => this.detector.noteBodyChildren(pageId, rawMarkdown)
30176
+ (pageId, rawMarkdown) => this.detector.noteBodyChildren(pageId, rawMarkdown),
30177
+ (folder, pageId, files, complete2) => this.puller.receiveFolderFiles(folder, pageId, files, complete2)
30012
30178
  );
30013
30179
  this.discovery = new DatabaseDiscovery(
30014
30180
  config,
@@ -30848,6 +31014,7 @@ var SyncOrchestrator = class {
30848
31014
  pendingDatabases
30849
31015
  );
30850
31016
  const finalize = async (created, updated, deleted, restored = 0) => {
31017
+ if (!options?.dryRun && this.detector.rootChanged()) await this.puller.receiveRootFiles();
30851
31018
  await this.puller.removeVacatedFolders();
30852
31019
  const renamedRows = this.databaseSyncer.takeRenamedRows();
30853
31020
  restoredPaths.push(...this.databaseSyncer.takeRestoredRows());