emdash-emit 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +96 -1
  2. package/README.md +259 -188
  3. package/dist/bin/emit.d.ts.map +1 -1
  4. package/dist/bin/emit.js +2 -0
  5. package/dist/bin/emit.js.map +1 -1
  6. package/dist/cli/commands/doctor.d.ts.map +1 -1
  7. package/dist/cli/commands/doctor.js +6 -0
  8. package/dist/cli/commands/doctor.js.map +1 -1
  9. package/dist/cli/commands/media.d.ts +4 -0
  10. package/dist/cli/commands/media.d.ts.map +1 -0
  11. package/dist/cli/commands/media.js +128 -0
  12. package/dist/cli/commands/media.js.map +1 -0
  13. package/dist/cli/commands/pull.d.ts +32 -0
  14. package/dist/cli/commands/pull.d.ts.map +1 -1
  15. package/dist/cli/commands/pull.js +136 -9
  16. package/dist/cli/commands/pull.js.map +1 -1
  17. package/dist/cli/commands/push.d.ts.map +1 -1
  18. package/dist/cli/commands/push.js +18 -0
  19. package/dist/cli/commands/push.js.map +1 -1
  20. package/dist/cli/commands/status.d.ts.map +1 -1
  21. package/dist/cli/commands/status.js +16 -5
  22. package/dist/cli/commands/status.js.map +1 -1
  23. package/dist/client/media.d.ts.map +1 -1
  24. package/dist/client/media.js +16 -5
  25. package/dist/client/media.js.map +1 -1
  26. package/dist/db/cache.d.ts +19 -4
  27. package/dist/db/cache.d.ts.map +1 -1
  28. package/dist/db/cache.js +127 -16
  29. package/dist/db/cache.js.map +1 -1
  30. package/dist/mcp/server.d.ts.map +1 -1
  31. package/dist/mcp/server.js +7 -3
  32. package/dist/mcp/server.js.map +1 -1
  33. package/dist/mcp/tools/sync-doctor.d.ts.map +1 -1
  34. package/dist/mcp/tools/sync-doctor.js +12 -0
  35. package/dist/mcp/tools/sync-doctor.js.map +1 -1
  36. package/dist/mcp/tools/sync-media-search.d.ts +4 -0
  37. package/dist/mcp/tools/sync-media-search.d.ts.map +1 -0
  38. package/dist/mcp/tools/sync-media-search.js +102 -0
  39. package/dist/mcp/tools/sync-media-search.js.map +1 -0
  40. package/dist/mcp/tools/sync-pull.d.ts.map +1 -1
  41. package/dist/mcp/tools/sync-pull.js +85 -13
  42. package/dist/mcp/tools/sync-pull.js.map +1 -1
  43. package/dist/mcp/tools/sync-push.d.ts.map +1 -1
  44. package/dist/mcp/tools/sync-push.js +5 -0
  45. package/dist/mcp/tools/sync-push.js.map +1 -1
  46. package/dist/mcp/tools/sync-status.d.ts.map +1 -1
  47. package/dist/mcp/tools/sync-status.js +6 -2
  48. package/dist/mcp/tools/sync-status.js.map +1 -1
  49. package/dist/sync/delete.d.ts +2 -1
  50. package/dist/sync/delete.d.ts.map +1 -1
  51. package/dist/sync/delete.js +6 -19
  52. package/dist/sync/delete.js.map +1 -1
  53. package/dist/sync/diff.d.ts.map +1 -1
  54. package/dist/sync/diff.js +18 -2
  55. package/dist/sync/diff.js.map +1 -1
  56. package/dist/sync/doctor.d.ts +27 -1
  57. package/dist/sync/doctor.d.ts.map +1 -1
  58. package/dist/sync/doctor.js +309 -26
  59. package/dist/sync/doctor.js.map +1 -1
  60. package/dist/sync/guards.d.ts +45 -0
  61. package/dist/sync/guards.d.ts.map +1 -0
  62. package/dist/sync/guards.js +236 -0
  63. package/dist/sync/guards.js.map +1 -0
  64. package/dist/sync/media/body_scan.d.ts +15 -0
  65. package/dist/sync/media/body_scan.d.ts.map +1 -0
  66. package/dist/sync/media/body_scan.js +132 -0
  67. package/dist/sync/media/body_scan.js.map +1 -0
  68. package/dist/sync/media/companions.d.ts +32 -0
  69. package/dist/sync/media/companions.d.ts.map +1 -0
  70. package/dist/sync/media/companions.js +260 -0
  71. package/dist/sync/media/companions.js.map +1 -0
  72. package/dist/sync/media/index.d.ts +9 -0
  73. package/dist/sync/media/index.d.ts.map +1 -0
  74. package/dist/sync/media/index.js +9 -0
  75. package/dist/sync/media/index.js.map +1 -0
  76. package/dist/sync/media/mirror_item.d.ts +27 -0
  77. package/dist/sync/media/mirror_item.d.ts.map +1 -0
  78. package/dist/sync/media/mirror_item.js +207 -0
  79. package/dist/sync/media/mirror_item.js.map +1 -0
  80. package/dist/sync/media/naming_mirror.d.ts +36 -0
  81. package/dist/sync/media/naming_mirror.d.ts.map +1 -0
  82. package/dist/sync/media/naming_mirror.js +143 -0
  83. package/dist/sync/media/naming_mirror.js.map +1 -0
  84. package/dist/sync/media/pull_article.d.ts +37 -0
  85. package/dist/sync/media/pull_article.d.ts.map +1 -0
  86. package/dist/sync/media/pull_article.js +273 -0
  87. package/dist/sync/media/pull_article.js.map +1 -0
  88. package/dist/sync/media/pull_library.d.ts +12 -0
  89. package/dist/sync/media/pull_library.d.ts.map +1 -0
  90. package/dist/sync/media/pull_library.js +255 -0
  91. package/dist/sync/media/pull_library.js.map +1 -0
  92. package/dist/sync/media/remote_index.d.ts +49 -0
  93. package/dist/sync/media/remote_index.d.ts.map +1 -0
  94. package/dist/sync/media/remote_index.js +241 -0
  95. package/dist/sync/media/remote_index.js.map +1 -0
  96. package/dist/sync/media/types.d.ts +237 -0
  97. package/dist/sync/media/types.d.ts.map +1 -0
  98. package/dist/sync/media/types.js +11 -0
  99. package/dist/sync/media/types.js.map +1 -0
  100. package/dist/sync/media.d.ts +1 -74
  101. package/dist/sync/media.d.ts.map +1 -1
  102. package/dist/sync/media.js +1 -592
  103. package/dist/sync/media.js.map +1 -1
  104. package/dist/sync/orchestrator.d.ts +23 -0
  105. package/dist/sync/orchestrator.d.ts.map +1 -1
  106. package/dist/sync/orchestrator.js +85 -1
  107. package/dist/sync/orchestrator.js.map +1 -1
  108. package/dist/sync/preflight.d.ts +1 -1
  109. package/dist/sync/preflight.d.ts.map +1 -1
  110. package/dist/sync/preflight.js +129 -17
  111. package/dist/sync/preflight.js.map +1 -1
  112. package/dist/sync/pull.d.ts +19 -0
  113. package/dist/sync/pull.d.ts.map +1 -1
  114. package/dist/sync/pull.js +169 -15
  115. package/dist/sync/pull.js.map +1 -1
  116. package/dist/sync/push.d.ts +7 -0
  117. package/dist/sync/push.d.ts.map +1 -1
  118. package/dist/sync/push.js +37 -10
  119. package/dist/sync/push.js.map +1 -1
  120. package/dist/sync/status.d.ts +4 -0
  121. package/dist/sync/status.d.ts.map +1 -1
  122. package/dist/sync/status.js +68 -4
  123. package/dist/sync/status.js.map +1 -1
  124. package/dist/utils/config.d.ts +25 -0
  125. package/dist/utils/config.d.ts.map +1 -1
  126. package/dist/utils/config.js +28 -2
  127. package/dist/utils/config.js.map +1 -1
  128. package/dist/utils/frontmatter.d.ts +1 -1
  129. package/dist/utils/frontmatter.d.ts.map +1 -1
  130. package/dist/utils/frontmatter.js +15 -4
  131. package/dist/utils/frontmatter.js.map +1 -1
  132. package/dist/utils/media-body-refs.d.ts +81 -0
  133. package/dist/utils/media-body-refs.d.ts.map +1 -0
  134. package/dist/utils/media-body-refs.js +240 -0
  135. package/dist/utils/media-body-refs.js.map +1 -0
  136. package/dist/utils/media-refs.d.ts +44 -0
  137. package/dist/utils/media-refs.d.ts.map +1 -0
  138. package/dist/utils/media-refs.js +101 -0
  139. package/dist/utils/media-refs.js.map +1 -0
  140. package/dist/utils/naming.d.ts +31 -0
  141. package/dist/utils/naming.d.ts.map +1 -1
  142. package/dist/utils/naming.js +58 -0
  143. package/dist/utils/naming.js.map +1 -1
  144. package/package.json +1 -1
@@ -0,0 +1,255 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { unwrapEnvelope } from '../../client/api.js';
4
+ import { ApiError } from '../../client/error.js';
5
+ import { createAuthedFetch } from '../../client/http.js';
6
+ import { DEFAULT_CONFIG } from '../../utils/config.js';
7
+ import { collectArticleStorageKeys } from './body_scan.js';
8
+ import { collectLocalMediaMirrorCandidates } from './companions.js';
9
+ import { syncSingleMediaItem } from './mirror_item.js';
10
+ import { mediaMirrorNames, mediaRecordIdForStem } from './naming_mirror.js';
11
+ import { pullArticleMedia } from './pull_article.js';
12
+ import { collectRemoteStorageKeys, findUnmirroredStorageKeys } from './remote_index.js';
13
+ import { MEDIA_LIST_PAGE_SIZE, } from './types.js';
14
+ /**
15
+ * Pulls media metadata and files from EmDash API into media/ directory with cursor pagination.
16
+ * media/<markdownRef>.yaml
17
+ * media/<markdownRef>.<ext>
18
+ *
19
+ * `markdownRef` is the sanitized storageKey basename (`storageKeyToMarkdownRef`), never the record id, so the
20
+ * mirror on disk shares one stem with the body URL that references it; the original `filename` survives only
21
+ * inside the companion YAML. See `mediaMirrorNames()` for the full naming and collision rule.
22
+ */
23
+ export async function pullMedia(options) {
24
+ if (options.storageKeys && options.storageKeys.length > 0) {
25
+ const fetchFn = createAuthedFetch(options);
26
+ const mediaDir = path.join(options.workspaceRoot, 'media');
27
+ fs.mkdirSync(mediaDir, { recursive: true });
28
+ const downloadFiles = options.downloadFiles !== false;
29
+ // Walk metadata via collectRemoteStorageKeys (walks library to index records by storageKey)
30
+ const remoteIndex = await collectRemoteStorageKeys({
31
+ workspaceRoot: options.workspaceRoot,
32
+ origin: options.origin,
33
+ token: options.token,
34
+ refreshToken: options.refreshToken,
35
+ fetchFn,
36
+ });
37
+ let totalProcessed = 0;
38
+ let downloadedCount = 0;
39
+ let skippedCount = 0;
40
+ const orphanStorageKeys = [];
41
+ for (const key of options.storageKeys) {
42
+ const item = remoteIndex.recordsByStorageKey.get(key);
43
+ if (!item) {
44
+ orphanStorageKeys.push(key);
45
+ continue;
46
+ }
47
+ totalProcessed++;
48
+ const result = await syncSingleMediaItem({
49
+ item,
50
+ mediaDir,
51
+ origin: options.origin,
52
+ token: options.token,
53
+ downloadFiles,
54
+ fetchFn,
55
+ cache: options.cache,
56
+ });
57
+ if (result.downloaded)
58
+ downloadedCount++;
59
+ else if (result.skipped)
60
+ skippedCount++;
61
+ }
62
+ return {
63
+ total: totalProcessed,
64
+ downloaded: downloadedCount,
65
+ skipped: skippedCount,
66
+ orphanStorageKeys: orphanStorageKeys.length > 0 ? orphanStorageKeys : undefined,
67
+ remoteTotalCount: remoteIndex.complete
68
+ ? (remoteIndex.totalCount ?? remoteIndex.scannedCount)
69
+ : remoteIndex.totalCount,
70
+ };
71
+ }
72
+ if (options.article) {
73
+ return pullArticleMedia({
74
+ workspaceRoot: options.workspaceRoot,
75
+ origin: options.origin,
76
+ token: options.token,
77
+ refreshToken: options.refreshToken,
78
+ article: options.article,
79
+ collection: options.collection,
80
+ downloadFiles: options.downloadFiles,
81
+ fetchFn: options.fetchFn,
82
+ cache: options.cache,
83
+ });
84
+ }
85
+ const fetchFn = createAuthedFetch(options);
86
+ const targetTotal = options.all ? Infinity : (options.limit ?? DEFAULT_CONFIG.mediaSyncLimit);
87
+ const downloadFiles = options.downloadFiles !== false;
88
+ const batchSize = MEDIA_LIST_PAGE_SIZE;
89
+ const mediaDir = path.join(options.workspaceRoot, 'media');
90
+ fs.mkdirSync(mediaDir, { recursive: true });
91
+ // Every mirrored companion is a confirmed storageKey source, including the ones written before Emit
92
+ // persisted the field, whose `url:` still names the key. Seeding from disk means `--prune` never
93
+ // deletes an asset that this run's media window simply did not reach.
94
+ const activeStorageKeys = new Set();
95
+ for (const candidate of collectLocalMediaMirrorCandidates(mediaDir).values()) {
96
+ if (candidate.storageKey)
97
+ activeStorageKeys.add(candidate.storageKey);
98
+ }
99
+ // Metadata for the coverage report and the storageKey census. `pullMedia` already walks the library
100
+ // cursor-by-cursor, so it owns the index instead of paying for a second one.
101
+ const remoteIndex = {
102
+ storageKeys: activeStorageKeys,
103
+ mediaIdsByStorageKey: new Map(),
104
+ recordsByStorageKey: new Map(),
105
+ recordsById: new Map(),
106
+ totalCount: null,
107
+ complete: false,
108
+ scannedCount: 0,
109
+ };
110
+ const activeMediaBaseNames = new Set();
111
+ // Only records the remote walk actually reports may authorise a mirror to stay. Seeding this from local
112
+ // companions instead would make every existing file self-exempt and prune nothing at all.
113
+ const activeMediaIds = new Set();
114
+ let totalProcessed = 0;
115
+ let downloadedCount = 0;
116
+ let skippedCount = 0;
117
+ let cursor = undefined;
118
+ while (totalProcessed < targetTotal) {
119
+ const remaining = targetTotal - totalProcessed;
120
+ const currentBatch = Math.min(remaining, batchSize);
121
+ const url = new URL(`/_emdash/api/media`, options.origin);
122
+ url.searchParams.set('limit', String(currentBatch));
123
+ if (cursor) {
124
+ url.searchParams.set('cursor', cursor);
125
+ }
126
+ const res = await fetchFn(url.toString(), {
127
+ method: 'GET',
128
+ headers: {
129
+ Authorization: `Bearer ${options.token}`,
130
+ Accept: 'application/json',
131
+ },
132
+ });
133
+ if (!res.ok) {
134
+ throw new ApiError({
135
+ message: `Failed to list media: ${res.statusText}`,
136
+ code: 'MEDIA_LIST_FAILED',
137
+ status: res.status,
138
+ endpoint: url.toString(),
139
+ suggestion: 'Verify network connection and token permissions.',
140
+ });
141
+ }
142
+ const json = await res.json();
143
+ const data = unwrapEnvelope(json);
144
+ if (typeof data.totalCount === 'number')
145
+ remoteIndex.totalCount = data.totalCount;
146
+ const rawItems = (Array.isArray(data.items) ? data.items : Array.isArray(data) ? data : []);
147
+ if (rawItems.length === 0) {
148
+ // An empty page ends the listing regardless of any cursor the server echoes back.
149
+ remoteIndex.complete = true;
150
+ break;
151
+ }
152
+ for (const item of rawItems) {
153
+ const names = mediaMirrorNames(item);
154
+ activeMediaBaseNames.add(names.markdownRef);
155
+ for (const legacy of names.legacyBaseNames) {
156
+ activeMediaBaseNames.add(legacy);
157
+ }
158
+ activeMediaIds.add(item.id);
159
+ if (item.storageKey) {
160
+ activeStorageKeys.add(item.storageKey);
161
+ remoteIndex.mediaIdsByStorageKey.set(item.storageKey, item.id);
162
+ }
163
+ const result = await syncSingleMediaItem({
164
+ item,
165
+ mediaDir,
166
+ origin: options.origin,
167
+ token: options.token,
168
+ downloadFiles,
169
+ fetchFn,
170
+ cache: options.cache,
171
+ });
172
+ if (result.downloaded)
173
+ downloadedCount++;
174
+ else if (result.skipped)
175
+ skippedCount++;
176
+ totalProcessed++;
177
+ remoteIndex.scannedCount++;
178
+ if (totalProcessed >= targetTotal) {
179
+ break;
180
+ }
181
+ }
182
+ if (!data.nextCursor || typeof data.nextCursor !== 'string') {
183
+ remoteIndex.complete = true;
184
+ break;
185
+ }
186
+ cursor = data.nextCursor;
187
+ }
188
+ // Pruning logic for media: if --prune or --all is enabled, delete local media not present on remote
189
+ let deletedCount = 0;
190
+ if ((options.prune || options.all) && fs.existsSync(mediaDir)) {
191
+ const files = fs.readdirSync(mediaDir);
192
+ for (const file of files) {
193
+ // Leftover `legacy_*` mirrors from the removed file-route fallback are never matched by the
194
+ // mediaId pattern below, so they survive pruning and must be deleted by hand.
195
+ if (file.startsWith('legacy_'))
196
+ continue;
197
+ // A mirror is identified with its companion YAML: the stem names the storageKey, and the record id
198
+ // lives inside the companion. Only a file Emit can attribute to a record may be pruned — an
199
+ // unattributable migrated name is kept, because deleting an asset over a guess is unrecoverable.
200
+ const stem = file.replace(/\.[^.]+$/, '');
201
+ if (activeMediaBaseNames.has(stem) || activeMediaBaseNames.has(stem.replace(/^\d{8}_/, ''))) {
202
+ continue;
203
+ }
204
+ const companionId = mediaRecordIdForStem(mediaDir, stem);
205
+ if (companionId && !activeMediaIds.has(companionId)) {
206
+ try {
207
+ fs.unlinkSync(path.join(mediaDir, file));
208
+ deletedCount++;
209
+ }
210
+ catch {
211
+ // Ignore unlink errors
212
+ }
213
+ }
214
+ }
215
+ }
216
+ // A library-wide walk is exhaustive, so it doubles as the storageKey census: every article reference
217
+ // that neither a local companion nor any remote record names is reported. The list endpoint exposes no
218
+ // storageKey filter, which is why the census requires the full walk `--all` already performs.
219
+ let referencedUnmirroredStorageKeys = undefined;
220
+ if (options.all) {
221
+ const referenced = new Set();
222
+ for (const status of ['draft', 'published']) {
223
+ const statusDir = path.join(options.workspaceRoot, options.collection || 'posts', status);
224
+ if (!fs.existsSync(statusDir))
225
+ continue;
226
+ for (const file of fs.readdirSync(statusDir)) {
227
+ if (!file.endsWith('.md'))
228
+ continue;
229
+ try {
230
+ const content = fs.readFileSync(path.join(statusDir, file), 'utf8');
231
+ for (const descriptor of collectArticleStorageKeys(content)) {
232
+ referenced.add(descriptor.storageKey);
233
+ }
234
+ }
235
+ catch {
236
+ // An unreadable article contributes nothing to the census.
237
+ }
238
+ }
239
+ }
240
+ if (referenced.size > 0) {
241
+ referencedUnmirroredStorageKeys = findUnmirroredStorageKeys(referenced, activeStorageKeys, remoteIndex);
242
+ }
243
+ }
244
+ return {
245
+ total: totalProcessed,
246
+ downloaded: downloadedCount,
247
+ skipped: skippedCount,
248
+ deleted: deletedCount,
249
+ remoteTotalCount: remoteIndex.complete
250
+ ? (remoteIndex.totalCount ?? remoteIndex.scannedCount)
251
+ : remoteIndex.totalCount,
252
+ referencedUnmirroredStorageKeys,
253
+ };
254
+ }
255
+ //# sourceMappingURL=pull_library.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pull_library.js","sourceRoot":"","sources":["../../../src/sync/media/pull_library.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { type LocalMirrorIndex, type MediaSearchResult, type RemoteStorageKeyIndex } from './types.js';
2
+ export declare const MEDIA_SEARCH_DEFAULT_LIMIT = 20;
3
+ export declare const MEDIA_SEARCH_MAX_LIMIT = 50;
4
+ /** Default scan ceiling. `maxScan: 0` means unlimited (a complete walk). */
5
+ export declare const MEDIA_SEARCH_DEFAULT_MAX_SCAN = 500;
6
+ export interface SearchRemoteMediaOptions {
7
+ workspaceRoot: string;
8
+ origin: string;
9
+ token: string;
10
+ refreshToken?: string;
11
+ /** Literal, case-insensitive substring match. Regex is never compiled from user input. */
12
+ query: string;
13
+ /** Max hits to return (default 20, clamped to 50). */
14
+ limit?: number;
15
+ /** Max records to read before stopping the walk (default 500; 0 = unlimited). */
16
+ maxScan?: number;
17
+ /** Restrict output to assets Emit already mirrors. The walk still covers the whole library. */
18
+ mirroredOnly?: boolean;
19
+ /** Decoration built by M2. When omitted, every hit reports mirrored: false. */
20
+ mirrorIndex?: LocalMirrorIndex;
21
+ fetchFn?: typeof fetch;
22
+ }
23
+ export declare function collectRemoteStorageKeys(options: {
24
+ workspaceRoot: string;
25
+ origin: string;
26
+ token: string;
27
+ refreshToken?: string;
28
+ /** Stop after this many records. Defaults to unlimited, which is the whole library. */
29
+ maxItems?: number;
30
+ fetchFn?: typeof fetch;
31
+ }): Promise<RemoteStorageKeyIndex>;
32
+ /**
33
+ * Searches the remote EmDash media library by keyword across filename, alt text, caption,
34
+ * storageKey, and mirrorStem.
35
+ *
36
+ * Adheres strictly to the 3 principles:
37
+ * 1. Does not send `q` to the server; walks and filters client-side.
38
+ * 2. Matches are literal substring comparisons (case-insensitive, no regex).
39
+ * 3. Epistemic completeness: Reports scannedCount and walkComplete.
40
+ */
41
+ export declare function searchRemoteMedia(options: SearchRemoteMediaOptions): Promise<MediaSearchResult>;
42
+ /**
43
+ * Reports body storageKeys that no local mirror names and that a full remote library walk does not
44
+ * contain. A key is only listed when the walk was complete: an early stop proves nothing about the
45
+ * rest of the library, so an incomplete walk yields nothing. Even a confirmed absence is reported as a
46
+ * lookup result rather than as a missing record — Emit mirrors records and never repairs server data.
47
+ */
48
+ export declare function findUnmirroredStorageKeys(referenced: Iterable<string>, localMirrorKeys: Iterable<string>, remoteIndex: RemoteStorageKeyIndex): string[];
49
+ //# sourceMappingURL=remote_index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote_index.d.ts","sourceRoot":"","sources":["../../../src/sync/media/remote_index.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,gBAAgB,EAErB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC3B,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAC7C,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,4EAA4E;AAC5E,eAAO,MAAM,6BAA6B,MAAM,CAAC;AAEjD,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAiDD,wBAAsB,wBAAwB,CAAC,OAAO,EAAE;IACtD,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CA4DjC;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,iBAAiB,CAAC,CAmI5B;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC5B,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,EACjC,WAAW,EAAE,qBAAqB,GACjC,MAAM,EAAE,CASV"}
@@ -0,0 +1,241 @@
1
+ import path from 'node:path';
2
+ import { unwrapEnvelope } from '../../client/api.js';
3
+ import { ApiError } from '../../client/error.js';
4
+ import { createAuthedFetch } from '../../client/http.js';
5
+ import { storageKeyToMarkdownRef } from '../../utils/naming.js';
6
+ import { MEDIA_LIST_PAGE_SIZE, } from './types.js';
7
+ export const MEDIA_SEARCH_DEFAULT_LIMIT = 20;
8
+ export const MEDIA_SEARCH_MAX_LIMIT = 50;
9
+ /** Default scan ceiling. `maxScan: 0` means unlimited (a complete walk). */
10
+ export const MEDIA_SEARCH_DEFAULT_MAX_SCAN = 500;
11
+ /**
12
+ * Fetches a single page from EmDash media list endpoint.
13
+ * Shared by collectRemoteStorageKeys and searchRemoteMedia to guarantee identical pagination behavior.
14
+ */
15
+ async function fetchMediaListPage(options) {
16
+ const url = new URL('/_emdash/api/media', options.origin);
17
+ url.searchParams.set('limit', String(options.limit));
18
+ if (options.cursor)
19
+ url.searchParams.set('cursor', options.cursor);
20
+ const res = await options.fetchFn(url.toString(), {
21
+ headers: {
22
+ Authorization: `Bearer ${options.token}`,
23
+ Accept: 'application/json',
24
+ },
25
+ });
26
+ if (!res.ok) {
27
+ throw new ApiError({
28
+ message: `Failed to list media: ${res.statusText}`,
29
+ code: 'MEDIA_LIST_FAILED',
30
+ status: res.status,
31
+ endpoint: url.toString(),
32
+ suggestion: 'Verify network connection and token permissions.',
33
+ });
34
+ }
35
+ const data = unwrapEnvelope(await res.json());
36
+ const totalCount = typeof data.totalCount === 'number' ? data.totalCount : null;
37
+ const rawItems = (Array.isArray(data.items) ? data.items : Array.isArray(data.data) ? data.data : []);
38
+ const nextCursor = typeof data.nextCursor === 'string' && data.nextCursor ? data.nextCursor : undefined;
39
+ return { rawItems, nextCursor, totalCount };
40
+ }
41
+ export async function collectRemoteStorageKeys(options) {
42
+ const fetchFn = createAuthedFetch(options);
43
+ const batchSize = MEDIA_LIST_PAGE_SIZE;
44
+ const maxItems = options.maxItems ?? Infinity;
45
+ const storageKeys = new Set();
46
+ const mediaIdsByStorageKey = new Map();
47
+ const recordsByStorageKey = new Map();
48
+ const recordsById = new Map();
49
+ let totalCount = null;
50
+ let scanned = 0;
51
+ let complete = false;
52
+ let cursor = undefined;
53
+ for (;;) {
54
+ const remaining = maxItems - scanned;
55
+ if (remaining <= 0)
56
+ break;
57
+ const currentBatch = Math.min(remaining, batchSize);
58
+ const page = await fetchMediaListPage({
59
+ origin: options.origin,
60
+ token: options.token,
61
+ limit: currentBatch,
62
+ cursor,
63
+ fetchFn,
64
+ });
65
+ if (page.totalCount !== null)
66
+ totalCount = page.totalCount;
67
+ for (const item of page.rawItems) {
68
+ scanned++;
69
+ if (item.id)
70
+ recordsById.set(item.id.toUpperCase(), item);
71
+ if (typeof item.storageKey === 'string' && item.storageKey) {
72
+ storageKeys.add(item.storageKey);
73
+ // First record wins a repeated key, so reporting a match stays deterministic.
74
+ if (item.id && !mediaIdsByStorageKey.has(item.storageKey)) {
75
+ mediaIdsByStorageKey.set(item.storageKey, item.id);
76
+ }
77
+ if (!recordsByStorageKey.has(item.storageKey)) {
78
+ recordsByStorageKey.set(item.storageKey, item);
79
+ }
80
+ }
81
+ }
82
+ if (page.rawItems.length === 0 || !page.nextCursor) {
83
+ complete = true;
84
+ break;
85
+ }
86
+ cursor = page.nextCursor;
87
+ }
88
+ return {
89
+ storageKeys,
90
+ mediaIdsByStorageKey,
91
+ recordsByStorageKey,
92
+ recordsById,
93
+ totalCount,
94
+ complete,
95
+ scannedCount: scanned,
96
+ };
97
+ }
98
+ /**
99
+ * Searches the remote EmDash media library by keyword across filename, alt text, caption,
100
+ * storageKey, and mirrorStem.
101
+ *
102
+ * Adheres strictly to the 3 principles:
103
+ * 1. Does not send `q` to the server; walks and filters client-side.
104
+ * 2. Matches are literal substring comparisons (case-insensitive, no regex).
105
+ * 3. Epistemic completeness: Reports scannedCount and walkComplete.
106
+ */
107
+ export async function searchRemoteMedia(options) {
108
+ const trimmed = options.query?.trim();
109
+ if (!trimmed) {
110
+ throw new ApiError({
111
+ message: 'A non-empty search query is required.',
112
+ code: 'MEDIA_SEARCH_QUERY_REQUIRED',
113
+ status: 400,
114
+ suggestion: 'Provide a search keyword (filename, alt text, caption, or storageKey).',
115
+ });
116
+ }
117
+ const normalizedQuery = trimmed.toLowerCase();
118
+ const rawLimit = options.limit ?? MEDIA_SEARCH_DEFAULT_LIMIT;
119
+ const limit = Math.max(1, Math.min(rawLimit, MEDIA_SEARCH_MAX_LIMIT));
120
+ const maxScan = options.maxScan === 0
121
+ ? Infinity
122
+ : typeof options.maxScan === 'number' && options.maxScan > 0
123
+ ? options.maxScan
124
+ : MEDIA_SEARCH_DEFAULT_MAX_SCAN;
125
+ const fetchFn = createAuthedFetch(options);
126
+ const batchSize = MEDIA_LIST_PAGE_SIZE;
127
+ const hits = [];
128
+ let scanned = 0;
129
+ let cursor = undefined;
130
+ let totalCount = null;
131
+ let walkComplete = false;
132
+ let matchCount = 0;
133
+ for (;;) {
134
+ const remaining = maxScan - scanned;
135
+ if (remaining <= 0) {
136
+ break;
137
+ }
138
+ const currentBatch = Math.min(remaining, batchSize);
139
+ const page = await fetchMediaListPage({
140
+ origin: options.origin,
141
+ token: options.token,
142
+ limit: currentBatch,
143
+ cursor,
144
+ fetchFn,
145
+ });
146
+ if (page.totalCount !== null) {
147
+ totalCount = page.totalCount;
148
+ }
149
+ for (const item of page.rawItems) {
150
+ scanned++;
151
+ const storageKey = item.storageKey || null;
152
+ const mirrorEntry = storageKey && options.mirrorIndex ? options.mirrorIndex.get(storageKey) : undefined;
153
+ const mirrored = Boolean(mirrorEntry);
154
+ if (options.mirroredOnly && !mirrored) {
155
+ continue;
156
+ }
157
+ const effectiveAlt = item.alt || mirrorEntry?.alt || null;
158
+ const effectiveCaption = item.caption || mirrorEntry?.caption || null;
159
+ const matchedOn = [];
160
+ if (item.filename && item.filename.toLowerCase().includes(normalizedQuery)) {
161
+ matchedOn.push('filename');
162
+ }
163
+ if (effectiveAlt && effectiveAlt.toLowerCase().includes(normalizedQuery)) {
164
+ matchedOn.push('alt');
165
+ }
166
+ if (effectiveCaption && effectiveCaption.toLowerCase().includes(normalizedQuery)) {
167
+ matchedOn.push('caption');
168
+ }
169
+ if (storageKey && storageKey.toLowerCase().includes(normalizedQuery)) {
170
+ matchedOn.push('storageKey');
171
+ }
172
+ if (storageKey) {
173
+ const ref = storageKeyToMarkdownRef(storageKey);
174
+ const stem = ref ? ref.toLowerCase() : '';
175
+ const localStem = mirrorEntry?.localPath
176
+ ? path
177
+ .basename(mirrorEntry.localPath)
178
+ .replace(/\.[^.]+$/, '')
179
+ .toLowerCase()
180
+ : '';
181
+ if ((stem && stem.includes(normalizedQuery)) ||
182
+ (localStem && localStem.includes(normalizedQuery))) {
183
+ if (!matchedOn.includes('mirrorStem') && !matchedOn.includes('storageKey')) {
184
+ matchedOn.push('mirrorStem');
185
+ }
186
+ }
187
+ }
188
+ if (matchedOn.length > 0) {
189
+ matchCount++;
190
+ if (hits.length < limit) {
191
+ hits.push({
192
+ mediaId: item.id.toUpperCase(),
193
+ storageKey,
194
+ filename: item.filename || null,
195
+ mimeType: item.mimeType || null,
196
+ size: typeof item.size === 'number' ? item.size : null,
197
+ alt: effectiveAlt,
198
+ caption: effectiveCaption,
199
+ createdAt: item.createdAt,
200
+ mirrored,
201
+ localPath: mirrorEntry?.localPath ?? null,
202
+ matchedOn,
203
+ });
204
+ }
205
+ }
206
+ }
207
+ if (page.rawItems.length === 0 || !page.nextCursor) {
208
+ walkComplete = true;
209
+ break;
210
+ }
211
+ cursor = page.nextCursor;
212
+ }
213
+ const truncated = matchCount > hits.length;
214
+ return {
215
+ hits,
216
+ scannedCount: scanned,
217
+ walkComplete,
218
+ totalCount,
219
+ truncated,
220
+ normalizedQuery,
221
+ };
222
+ }
223
+ /**
224
+ * Reports body storageKeys that no local mirror names and that a full remote library walk does not
225
+ * contain. A key is only listed when the walk was complete: an early stop proves nothing about the
226
+ * rest of the library, so an incomplete walk yields nothing. Even a confirmed absence is reported as a
227
+ * lookup result rather than as a missing record — Emit mirrors records and never repairs server data.
228
+ */
229
+ export function findUnmirroredStorageKeys(referenced, localMirrorKeys, remoteIndex) {
230
+ if (!remoteIndex.complete)
231
+ return [];
232
+ const mirrored = new Set(localMirrorKeys);
233
+ const unresolved = new Set();
234
+ for (const key of referenced) {
235
+ if (mirrored.has(key) || remoteIndex.storageKeys.has(key))
236
+ continue;
237
+ unresolved.add(key);
238
+ }
239
+ return Array.from(unresolved).sort();
240
+ }
241
+ //# sourceMappingURL=remote_index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote_index.js","sourceRoot":"","sources":["../../../src/sync/media/remote_index.ts"],"names":[],"mappings":"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"}