granola-toolkit 0.32.0 → 0.34.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/README.md +15 -3
- package/dist/cli.js +307 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ Export notes:
|
|
|
79
79
|
```bash
|
|
80
80
|
granola auth login
|
|
81
81
|
granola notes
|
|
82
|
+
granola notes --folder Team
|
|
82
83
|
|
|
83
84
|
node dist/cli.js notes --supabase "$HOME/Library/Application Support/Granola/supabase.json"
|
|
84
85
|
node dist/cli.js notes --format json --output ./notes-json
|
|
@@ -91,6 +92,7 @@ Export transcripts:
|
|
|
91
92
|
```bash
|
|
92
93
|
node dist/cli.js transcripts --cache "$HOME/Library/Application Support/Granola/cache-v3.json"
|
|
93
94
|
node dist/cli.js transcripts --format yaml --output ./transcripts-yaml
|
|
95
|
+
granola transcripts --folder Team
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
Inspect individual meetings:
|
|
@@ -143,6 +145,8 @@ The flow is:
|
|
|
143
145
|
6. render that export as Markdown, JSON, YAML, or raw JSON
|
|
144
146
|
7. write one file per document into the output directory
|
|
145
147
|
|
|
148
|
+
When you pass `--folder <id|name>`, the export is filtered to that folder and, by default, written into a stable per-folder subdirectory under the notes output root.
|
|
149
|
+
|
|
146
150
|
Content is chosen in this order:
|
|
147
151
|
|
|
148
152
|
1. `notes`
|
|
@@ -169,6 +173,8 @@ The flow is:
|
|
|
169
173
|
5. render each export as text, JSON, YAML, or raw JSON
|
|
170
174
|
6. write one file per document into the output directory
|
|
171
175
|
|
|
176
|
+
When you pass `--folder <id|name>`, the export is filtered to that folder and, by default, written into a stable per-folder subdirectory under the transcripts output root.
|
|
177
|
+
|
|
172
178
|
Speaker labels are currently normalised to:
|
|
173
179
|
|
|
174
180
|
- `You` for `microphone`
|
|
@@ -223,6 +229,8 @@ The current CLI surface includes:
|
|
|
223
229
|
- `folder list`
|
|
224
230
|
- `folder view <id|name>`
|
|
225
231
|
- `meeting list --folder <id|name>`
|
|
232
|
+
- `notes --folder <id|name>`
|
|
233
|
+
- `transcripts --folder <id|name>`
|
|
226
234
|
|
|
227
235
|
### Server
|
|
228
236
|
|
|
@@ -250,9 +258,9 @@ The initial server API includes:
|
|
|
250
258
|
- `POST /auth/logout`
|
|
251
259
|
- `POST /auth/mode`
|
|
252
260
|
- `POST /auth/refresh`
|
|
253
|
-
- `POST /exports/notes`
|
|
261
|
+
- `POST /exports/notes` with optional `folderId`
|
|
254
262
|
- `POST /exports/jobs/:id/rerun`
|
|
255
|
-
- `POST /exports/transcripts`
|
|
263
|
+
- `POST /exports/transcripts` with optional `folderId`
|
|
256
264
|
|
|
257
265
|
This is the shared runtime for `granola web` and `granola attach`.
|
|
258
266
|
|
|
@@ -287,6 +295,7 @@ The initial browser client includes:
|
|
|
287
295
|
- app-state status from the shared core
|
|
288
296
|
- an auth session panel for login, refresh, source switching, and sign-out
|
|
289
297
|
- note and transcript export actions backed by the same local API
|
|
298
|
+
- folder-scoped export actions that follow the currently selected folder
|
|
290
299
|
- a recent export-jobs panel with rerun actions
|
|
291
300
|
- stronger empty and error states for list/detail failures
|
|
292
301
|
- a server-access panel that can unlock or lock a password-protected local server
|
|
@@ -319,6 +328,7 @@ That keeps the current single-package repo simple, while making a future split i
|
|
|
319
328
|
|
|
320
329
|
The initial terminal workspace includes:
|
|
321
330
|
|
|
331
|
+
- a folder scope inside the navigation pane, including an explicit All meetings view
|
|
322
332
|
- a meeting list pane with keyboard navigation
|
|
323
333
|
- a detail pane with notes, transcript, metadata, and raw views
|
|
324
334
|
- an auth session overlay for import, refresh, source switching, and sign-out
|
|
@@ -327,7 +337,8 @@ The initial terminal workspace includes:
|
|
|
327
337
|
|
|
328
338
|
The main keyboard controls are:
|
|
329
339
|
|
|
330
|
-
- `
|
|
340
|
+
- `h` / `l`, left / right, or `Tab` to switch between folders and meetings
|
|
341
|
+
- `j` / `k` or arrow keys to move within the active folder or meeting list
|
|
331
342
|
- `/` or `Ctrl+P` to open quick open
|
|
332
343
|
- `a` to open auth session actions
|
|
333
344
|
- `1`-`4` to switch detail tabs
|
|
@@ -352,6 +363,7 @@ The web client uses the index as a fast path and upgrades to live data automatic
|
|
|
352
363
|
Exports are now tracked as jobs with:
|
|
353
364
|
|
|
354
365
|
- persistent local history across CLI and web runs
|
|
366
|
+
- explicit scope metadata for all-meetings and folder-scoped runs
|
|
355
367
|
- running, completed, and failed status
|
|
356
368
|
- per-export progress counters
|
|
357
369
|
- rerun support from `granola exports rerun <job-id>` or the web client
|
package/dist/cli.js
CHANGED
|
@@ -218,16 +218,22 @@ var GranolaServerClient = class GranolaServerClient {
|
|
|
218
218
|
async listExportJobs(options = {}) {
|
|
219
219
|
return await this.requestJson(granolaExportJobsPath(options));
|
|
220
220
|
}
|
|
221
|
-
async exportNotes(format = "markdown") {
|
|
221
|
+
async exportNotes(format = "markdown", options = {}) {
|
|
222
222
|
return await this.requestJson(granolaTransportPaths.exportNotes, {
|
|
223
|
-
body: JSON.stringify({
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
folderId: options.folderId,
|
|
225
|
+
format
|
|
226
|
+
}),
|
|
224
227
|
headers: { "content-type": "application/json" },
|
|
225
228
|
method: "POST"
|
|
226
229
|
});
|
|
227
230
|
}
|
|
228
|
-
async exportTranscripts(format = "text") {
|
|
231
|
+
async exportTranscripts(format = "text", options = {}) {
|
|
229
232
|
return await this.requestJson(granolaTransportPaths.exportTranscripts, {
|
|
230
|
-
body: JSON.stringify({
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
folderId: options.folderId,
|
|
235
|
+
format
|
|
236
|
+
}),
|
|
231
237
|
headers: { "content-type": "application/json" },
|
|
232
238
|
method: "POST"
|
|
233
239
|
});
|
|
@@ -1345,6 +1351,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1345
1351
|
`ID: ${summary.id}`,
|
|
1346
1352
|
`Created: ${summary.createdAt}`,
|
|
1347
1353
|
`Updated: ${summary.updatedAt}`,
|
|
1354
|
+
`Folders: ${summary.folders.length > 0 ? summary.folders.map((folder) => folder.name).join(", ") : "none"}`,
|
|
1348
1355
|
`Tags: ${summary.tags.length > 0 ? summary.tags.join(", ") : "none"}`,
|
|
1349
1356
|
`Notes source: ${summary.noteContentSource}`,
|
|
1350
1357
|
`Transcript loaded: ${summary.transcriptLoaded ? "yes" : "no"}`,
|
|
@@ -1360,7 +1367,7 @@ function renderGranolaTuiMeetingTab(bundle, tab) {
|
|
|
1360
1367
|
}
|
|
1361
1368
|
}
|
|
1362
1369
|
function buildGranolaTuiSummary(state, meetingSource) {
|
|
1363
|
-
return `auth ${state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | list ${meetingSource}`;
|
|
1370
|
+
return `auth ${state.auth.mode === "stored-session" ? "stored" : "supabase"} | ${state.documents.loaded ? `${state.documents.count} docs` : "docs pending"} | ${state.folders.loaded ? `${state.folders.count} folders` : "folders pending"} | ${state.cache.loaded ? `${state.cache.transcriptCount} transcript sets` : state.cache.configured ? "cache configured" : "cache missing"} | ${state.index.loaded ? `${state.index.meetingCount} indexed` : "index pending"} | list ${meetingSource}`;
|
|
1364
1371
|
}
|
|
1365
1372
|
//#endregion
|
|
1366
1373
|
//#region src/tui/theme.ts
|
|
@@ -1657,9 +1664,13 @@ var GranolaTuiWorkspace = class {
|
|
|
1657
1664
|
focused = false;
|
|
1658
1665
|
#maxMeetings;
|
|
1659
1666
|
#appState;
|
|
1667
|
+
#activePane = "meetings";
|
|
1660
1668
|
#detailError = "";
|
|
1661
1669
|
#detailScroll = 0;
|
|
1662
1670
|
#detailToken = 0;
|
|
1671
|
+
#folderError = "";
|
|
1672
|
+
#folderToken = 0;
|
|
1673
|
+
#folders = [];
|
|
1663
1674
|
#listError = "";
|
|
1664
1675
|
#listToken = 0;
|
|
1665
1676
|
#loadingDetail = false;
|
|
@@ -1667,6 +1678,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1667
1678
|
#meetingSource = "live";
|
|
1668
1679
|
#meetings = [];
|
|
1669
1680
|
#overlay;
|
|
1681
|
+
#selectedFolderId;
|
|
1670
1682
|
#selectedMeeting;
|
|
1671
1683
|
#selectedMeetingId;
|
|
1672
1684
|
#statusMessage = "Loading meetings…";
|
|
@@ -1679,11 +1691,13 @@ var GranolaTuiWorkspace = class {
|
|
|
1679
1691
|
this.options = options;
|
|
1680
1692
|
this.#appState = app.getState();
|
|
1681
1693
|
this.#maxMeetings = options.maxMeetings ?? 200;
|
|
1694
|
+
this.#selectedFolderId = this.#appState.ui.selectedFolderId;
|
|
1682
1695
|
}
|
|
1683
1696
|
async initialise() {
|
|
1684
1697
|
this.#unsubscribe = this.app.subscribe((event) => {
|
|
1685
1698
|
this.handleAppUpdate(event);
|
|
1686
1699
|
});
|
|
1700
|
+
await this.loadFolders({ setStatus: false });
|
|
1687
1701
|
await this.loadMeetings({
|
|
1688
1702
|
preferredMeetingId: this.options.initialMeetingId,
|
|
1689
1703
|
setStatus: true
|
|
@@ -1699,7 +1713,12 @@ var GranolaTuiWorkspace = class {
|
|
|
1699
1713
|
handleAppUpdate(event) {
|
|
1700
1714
|
const previousDocumentsLoadedAt = this.#appState.documents.loadedAt;
|
|
1701
1715
|
this.#appState = event.state;
|
|
1702
|
-
|
|
1716
|
+
this.#selectedFolderId = event.state.ui.selectedFolderId;
|
|
1717
|
+
this.#selectedMeetingId = event.state.ui.selectedMeetingId ?? this.#selectedMeetingId;
|
|
1718
|
+
if (this.#meetingSource === "index" && event.state.documents.loadedAt && event.state.documents.loadedAt !== previousDocumentsLoadedAt && !this.#loadingMeetings) (async () => {
|
|
1719
|
+
await this.loadFolders({ setStatus: false });
|
|
1720
|
+
await this.loadMeetings({ preferredMeetingId: this.#selectedMeetingId });
|
|
1721
|
+
})();
|
|
1703
1722
|
this.tui.requestRender();
|
|
1704
1723
|
}
|
|
1705
1724
|
setStatus(message, tone = "info") {
|
|
@@ -1712,6 +1731,11 @@ var GranolaTuiWorkspace = class {
|
|
|
1712
1731
|
const selectedIndex = this.#selectedMeetingId ? this.#meetings.findIndex((meeting) => meeting.id === this.#selectedMeetingId) : -1;
|
|
1713
1732
|
return selectedIndex >= 0 ? selectedIndex : 0;
|
|
1714
1733
|
}
|
|
1734
|
+
normaliseSelectedFolderIndex() {
|
|
1735
|
+
if (!this.#selectedFolderId) return 0;
|
|
1736
|
+
const selectedIndex = this.#folders.findIndex((folder) => folder.id === this.#selectedFolderId);
|
|
1737
|
+
return selectedIndex >= 0 ? selectedIndex + 1 : 0;
|
|
1738
|
+
}
|
|
1715
1739
|
ensureMeetingVisible(meeting) {
|
|
1716
1740
|
const existingIndex = this.#meetings.findIndex((item) => item.id === meeting.id);
|
|
1717
1741
|
if (existingIndex >= 0) this.#meetings[existingIndex] = meeting;
|
|
@@ -1721,6 +1745,30 @@ var GranolaTuiWorkspace = class {
|
|
|
1721
1745
|
return left.title.localeCompare(right.title);
|
|
1722
1746
|
});
|
|
1723
1747
|
}
|
|
1748
|
+
async loadFolders(options = {}) {
|
|
1749
|
+
const token = ++this.#folderToken;
|
|
1750
|
+
this.#folderError = "";
|
|
1751
|
+
if (options.setStatus) this.setStatus(options.forceRefresh ? "Refreshing folders…" : "Loading folders…");
|
|
1752
|
+
try {
|
|
1753
|
+
const result = await this.app.listFolders({
|
|
1754
|
+
forceRefresh: options.forceRefresh,
|
|
1755
|
+
limit: 100
|
|
1756
|
+
});
|
|
1757
|
+
if (token !== this.#folderToken) return;
|
|
1758
|
+
this.#folders = result.folders;
|
|
1759
|
+
if (this.#selectedFolderId && !this.#folders.some((folder) => folder.id === this.#selectedFolderId)) this.#selectedFolderId = void 0;
|
|
1760
|
+
this.#folderError = "";
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
if (token !== this.#folderToken) return;
|
|
1763
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1764
|
+
this.#folderError = message;
|
|
1765
|
+
this.#folders = [];
|
|
1766
|
+
this.#selectedFolderId = void 0;
|
|
1767
|
+
this.setStatus(message, "error");
|
|
1768
|
+
} finally {
|
|
1769
|
+
if (token === this.#folderToken) this.tui.requestRender();
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1724
1772
|
async loadMeetings(options = {}) {
|
|
1725
1773
|
const token = ++this.#listToken;
|
|
1726
1774
|
this.#loadingMeetings = true;
|
|
@@ -1728,6 +1776,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1728
1776
|
if (options.setStatus !== false) this.setStatus(options.forceRefresh ? "Refreshing meetings…" : "Loading meetings…");
|
|
1729
1777
|
try {
|
|
1730
1778
|
const result = await this.app.listMeetings({
|
|
1779
|
+
folderId: this.#selectedFolderId,
|
|
1731
1780
|
forceRefresh: options.forceRefresh,
|
|
1732
1781
|
limit: this.#maxMeetings,
|
|
1733
1782
|
preferIndex: true
|
|
@@ -1736,8 +1785,13 @@ var GranolaTuiWorkspace = class {
|
|
|
1736
1785
|
this.#meetings = result.meetings;
|
|
1737
1786
|
this.#meetingSource = result.source;
|
|
1738
1787
|
this.#selectedMeetingId = options.preferredMeetingId && this.#meetings.some((meeting) => meeting.id === options.preferredMeetingId) ? options.preferredMeetingId : this.#selectedMeetingId && this.#meetings.some((meeting) => meeting.id === this.#selectedMeetingId) ? this.#selectedMeetingId : this.#meetings[0]?.id;
|
|
1788
|
+
if (!this.#selectedMeetingId) {
|
|
1789
|
+
this.#selectedMeeting = void 0;
|
|
1790
|
+
this.#detailError = "";
|
|
1791
|
+
this.#detailScroll = 0;
|
|
1792
|
+
}
|
|
1739
1793
|
this.#listError = "";
|
|
1740
|
-
this.setStatus(result.source === "index" ? "Loaded meetings from the local index" : "Connected to Granola");
|
|
1794
|
+
this.setStatus(result.source === "index" ? "Loaded meetings from the local index" : this.#selectedFolderId ? "Connected to Granola (folder scope)" : "Connected to Granola");
|
|
1741
1795
|
} catch (error) {
|
|
1742
1796
|
if (token !== this.#listToken) return;
|
|
1743
1797
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1780,6 +1834,10 @@ var GranolaTuiWorkspace = class {
|
|
|
1780
1834
|
}
|
|
1781
1835
|
async refresh(forceRefresh) {
|
|
1782
1836
|
try {
|
|
1837
|
+
await this.loadFolders({
|
|
1838
|
+
forceRefresh,
|
|
1839
|
+
setStatus: false
|
|
1840
|
+
});
|
|
1783
1841
|
await this.loadMeetings({
|
|
1784
1842
|
forceRefresh,
|
|
1785
1843
|
preferredMeetingId: this.#selectedMeetingId
|
|
@@ -1787,7 +1845,7 @@ var GranolaTuiWorkspace = class {
|
|
|
1787
1845
|
if (this.#selectedMeetingId) await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
|
|
1788
1846
|
} catch {}
|
|
1789
1847
|
}
|
|
1790
|
-
async
|
|
1848
|
+
async moveMeetingSelection(delta) {
|
|
1791
1849
|
if (this.#meetings.length === 0) return;
|
|
1792
1850
|
const currentIndex = this.normaliseSelectedIndex();
|
|
1793
1851
|
const nextIndex = Math.max(0, Math.min(this.#meetings.length - 1, currentIndex + delta));
|
|
@@ -1795,6 +1853,33 @@ var GranolaTuiWorkspace = class {
|
|
|
1795
1853
|
if (!nextMeeting || nextMeeting.id === this.#selectedMeetingId) return;
|
|
1796
1854
|
await this.loadMeeting(nextMeeting.id);
|
|
1797
1855
|
}
|
|
1856
|
+
async moveFolderSelection(delta) {
|
|
1857
|
+
const total = this.#folders.length + 1;
|
|
1858
|
+
const currentIndex = this.normaliseSelectedFolderIndex();
|
|
1859
|
+
const nextIndex = Math.max(0, Math.min(total - 1, currentIndex + delta));
|
|
1860
|
+
const nextFolderId = nextIndex === 0 ? void 0 : this.#folders[nextIndex - 1]?.id;
|
|
1861
|
+
if (nextFolderId === this.#selectedFolderId) return;
|
|
1862
|
+
this.#selectedFolderId = nextFolderId;
|
|
1863
|
+
this.#selectedMeeting = void 0;
|
|
1864
|
+
this.#detailError = "";
|
|
1865
|
+
this.#detailScroll = 0;
|
|
1866
|
+
await this.loadMeetings({
|
|
1867
|
+
preferredMeetingId: this.#selectedMeetingId,
|
|
1868
|
+
setStatus: false
|
|
1869
|
+
});
|
|
1870
|
+
if (this.#selectedMeetingId) {
|
|
1871
|
+
await this.loadMeeting(this.#selectedMeetingId, { ensureMeetingVisible: true });
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
this.tui.requestRender();
|
|
1875
|
+
}
|
|
1876
|
+
async moveSelection(delta) {
|
|
1877
|
+
if (this.#activePane === "folders") {
|
|
1878
|
+
await this.moveFolderSelection(delta);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
await this.moveMeetingSelection(delta);
|
|
1882
|
+
}
|
|
1798
1883
|
currentDetailBody(width) {
|
|
1799
1884
|
if (this.#detailError) return wrapBlock(this.#detailError, width);
|
|
1800
1885
|
if (this.#loadingDetail && !this.#selectedMeeting) return wrapBlock("Loading meeting details…", width);
|
|
@@ -1832,6 +1917,10 @@ var GranolaTuiWorkspace = class {
|
|
|
1832
1917
|
async reloadAfterAuthChange() {
|
|
1833
1918
|
const preferredMeetingId = this.#selectedMeeting?.document.id ?? this.#selectedMeetingId;
|
|
1834
1919
|
try {
|
|
1920
|
+
await this.loadFolders({
|
|
1921
|
+
forceRefresh: true,
|
|
1922
|
+
setStatus: false
|
|
1923
|
+
});
|
|
1835
1924
|
await this.loadMeetings({
|
|
1836
1925
|
forceRefresh: true,
|
|
1837
1926
|
preferredMeetingId,
|
|
@@ -1956,6 +2045,21 @@ var GranolaTuiWorkspace = class {
|
|
|
1956
2045
|
this.openAuthPanel();
|
|
1957
2046
|
return;
|
|
1958
2047
|
}
|
|
2048
|
+
if (matchesKey(data, "tab")) {
|
|
2049
|
+
this.#activePane = this.#activePane === "folders" ? "meetings" : "folders";
|
|
2050
|
+
this.tui.requestRender();
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (matchesKey(data, "left") || matchesKey(data, "h")) {
|
|
2054
|
+
this.#activePane = "folders";
|
|
2055
|
+
this.tui.requestRender();
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (matchesKey(data, "right") || matchesKey(data, "l")) {
|
|
2059
|
+
this.#activePane = "meetings";
|
|
2060
|
+
this.tui.requestRender();
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
1959
2063
|
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
1960
2064
|
this.moveSelection(-1);
|
|
1961
2065
|
return;
|
|
@@ -2021,22 +2125,53 @@ var GranolaTuiWorkspace = class {
|
|
|
2021
2125
|
renderListPane(width, height) {
|
|
2022
2126
|
const lines = [];
|
|
2023
2127
|
const innerWidth = Math.max(1, width - 2);
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2128
|
+
const folderEntries = [{
|
|
2129
|
+
id: void 0,
|
|
2130
|
+
label: "All meetings",
|
|
2131
|
+
meta: this.#folders.length > 0 ? `${this.#folders.length} folders` : "global scope"
|
|
2132
|
+
}, ...this.#folders.map((folder) => ({
|
|
2133
|
+
id: folder.id,
|
|
2134
|
+
label: `${folder.isFavourite ? "★ " : ""}${folder.name || folder.id}`,
|
|
2135
|
+
meta: `${folder.documentCount} meetings`
|
|
2136
|
+
}))];
|
|
2137
|
+
const availableRows = Math.max(2, height - 3);
|
|
2138
|
+
const folderWindowSize = Math.min(Math.max(3, Math.min(8, Math.floor(availableRows * .35))), Math.max(1, availableRows - 1));
|
|
2139
|
+
const meetingWindowSize = Math.max(1, availableRows - folderWindowSize);
|
|
2140
|
+
const folderHeader = `${this.#activePane === "folders" ? granolaTuiTheme.accent("Folders") : granolaTuiTheme.strong("Folders")} ${granolaTuiTheme.dim(`(${this.#folders.length})`)}`;
|
|
2141
|
+
lines.push(padLine(folderHeader, innerWidth));
|
|
2142
|
+
if (this.#folderError) {
|
|
2143
|
+
lines.push(...wrapBlock(granolaTuiTheme.error(this.#folderError), innerWidth).slice(0, folderWindowSize));
|
|
2144
|
+
while (lines.length < 1 + folderWindowSize) lines.push(" ".repeat(innerWidth));
|
|
2145
|
+
} else {
|
|
2146
|
+
const selectedFolderIndex = this.normaliseSelectedFolderIndex();
|
|
2147
|
+
const folderStartIndex = Math.max(0, Math.min(selectedFolderIndex - Math.floor(folderWindowSize / 2), folderEntries.length - folderWindowSize));
|
|
2148
|
+
const visibleFolders = folderEntries.slice(folderStartIndex, folderStartIndex + folderWindowSize);
|
|
2149
|
+
for (const [offset, folder] of visibleFolders.entries()) {
|
|
2150
|
+
const selected = folderStartIndex + offset === selectedFolderIndex;
|
|
2151
|
+
const prefix = selected ? "> " : " ";
|
|
2152
|
+
const maxLabelWidth = Math.max(6, innerWidth - visibleWidth(prefix) - visibleWidth(folder.meta) - 1);
|
|
2153
|
+
const labelBlock = `${prefix}${truncateToWidth(folder.label, maxLabelWidth, "")}`;
|
|
2154
|
+
const line = `${labelBlock}${" ".repeat(Math.max(1, innerWidth - visibleWidth(labelBlock) - visibleWidth(folder.meta)))}${granolaTuiTheme.dim(folder.meta)}`;
|
|
2155
|
+
lines.push(selected ? padLine(granolaTuiTheme.selected(line), innerWidth) : padLine(line, innerWidth));
|
|
2156
|
+
}
|
|
2157
|
+
while (lines.length < 1 + folderWindowSize) lines.push(" ".repeat(innerWidth));
|
|
2158
|
+
}
|
|
2159
|
+
lines.push(padLine(granolaTuiTheme.dim(""), innerWidth));
|
|
2160
|
+
const meetingsHeader = `${this.#activePane === "meetings" ? granolaTuiTheme.accent("Meetings") : granolaTuiTheme.strong("Meetings")} ${granolaTuiTheme.dim(`(${this.#meetings.length})`)}`;
|
|
2161
|
+
lines.push(padLine(meetingsHeader, innerWidth));
|
|
2026
2162
|
if (this.#listError) {
|
|
2027
|
-
lines.push(...wrapBlock(granolaTuiTheme.error(this.#listError), innerWidth).slice(0,
|
|
2163
|
+
lines.push(...wrapBlock(granolaTuiTheme.error(this.#listError), innerWidth).slice(0, meetingWindowSize));
|
|
2028
2164
|
while (lines.length < height) lines.push(" ".repeat(innerWidth));
|
|
2029
2165
|
return lines;
|
|
2030
2166
|
}
|
|
2031
2167
|
if (this.#meetings.length === 0) {
|
|
2032
|
-
lines.push(...wrapBlock("No meetings available yet.", innerWidth).slice(0,
|
|
2168
|
+
lines.push(...wrapBlock("No meetings available yet.", innerWidth).slice(0, meetingWindowSize));
|
|
2033
2169
|
while (lines.length < height) lines.push(" ".repeat(innerWidth));
|
|
2034
2170
|
return lines;
|
|
2035
2171
|
}
|
|
2036
2172
|
const selectedIndex = this.normaliseSelectedIndex();
|
|
2037
|
-
const
|
|
2038
|
-
const
|
|
2039
|
-
const visibleMeetings = this.#meetings.slice(startIndex, startIndex + windowSize);
|
|
2173
|
+
const startIndex = Math.max(0, Math.min(selectedIndex - Math.floor(meetingWindowSize / 2), this.#meetings.length - meetingWindowSize));
|
|
2174
|
+
const visibleMeetings = this.#meetings.slice(startIndex, startIndex + meetingWindowSize);
|
|
2040
2175
|
for (const [offset, meeting] of visibleMeetings.entries()) {
|
|
2041
2176
|
const selected = startIndex + offset === selectedIndex;
|
|
2042
2177
|
const dateLabel = meeting.updatedAt.slice(0, 10);
|
|
@@ -2094,7 +2229,7 @@ var GranolaTuiWorkspace = class {
|
|
|
2094
2229
|
const bodyLines = [];
|
|
2095
2230
|
for (let index = 0; index < bodyHeight; index += 1) bodyLines.push(`${padLine(listLines[index] ?? "", listWidth)} | ${padLine(detailLines[index] ?? "", detailWidth)}`);
|
|
2096
2231
|
const footerStatus = padLine(toneText(this.#statusTone, this.#statusMessage), width);
|
|
2097
|
-
const footerHints = padLine(granolaTuiTheme.dim("/ quick open a auth r refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
2232
|
+
const footerHints = padLine(granolaTuiTheme.dim("h/l or Tab pane j/k move / quick open a auth r refresh 1-4 tabs PgUp/PgDn scroll q quit"), width);
|
|
2098
2233
|
return [
|
|
2099
2234
|
headerTitle,
|
|
2100
2235
|
headerSummary,
|
|
@@ -2845,6 +2980,42 @@ async function loadOptionalGranolaCache(cacheFile) {
|
|
|
2845
2980
|
return parseCacheContents(await readFile(cacheFile, "utf8"));
|
|
2846
2981
|
}
|
|
2847
2982
|
//#endregion
|
|
2983
|
+
//#region src/export-scope.ts
|
|
2984
|
+
const FOLDER_EXPORT_DIRECTORY = "_folders";
|
|
2985
|
+
function allExportScope() {
|
|
2986
|
+
return { mode: "all" };
|
|
2987
|
+
}
|
|
2988
|
+
function folderExportScope(folder) {
|
|
2989
|
+
return {
|
|
2990
|
+
folderId: folder.id,
|
|
2991
|
+
folderName: folder.name || folder.id,
|
|
2992
|
+
mode: "folder"
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
function cloneExportScope(scope) {
|
|
2996
|
+
return scope.mode === "folder" ? { ...scope } : { mode: "all" };
|
|
2997
|
+
}
|
|
2998
|
+
function normaliseExportScope(value) {
|
|
2999
|
+
const record = asRecord(value);
|
|
3000
|
+
if (!record) return allExportScope();
|
|
3001
|
+
if (record.mode !== "folder") return allExportScope();
|
|
3002
|
+
const folderId = stringValue(record.folderId);
|
|
3003
|
+
const folderName = stringValue(record.folderName) || folderId;
|
|
3004
|
+
if (!folderId) return allExportScope();
|
|
3005
|
+
return {
|
|
3006
|
+
folderId,
|
|
3007
|
+
folderName,
|
|
3008
|
+
mode: "folder"
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
function renderExportScopeLabel(scope) {
|
|
3012
|
+
return scope.mode === "folder" ? `folder ${scope.folderName}` : "all meetings";
|
|
3013
|
+
}
|
|
3014
|
+
function resolveExportOutputDir(outputDir, scope, options = {}) {
|
|
3015
|
+
if (scope.mode !== "folder" || options.scopedDirectory === false) return outputDir;
|
|
3016
|
+
return join(outputDir, FOLDER_EXPORT_DIRECTORY, sanitiseFilename(scope.folderId, "folder"));
|
|
3017
|
+
}
|
|
3018
|
+
//#endregion
|
|
2848
3019
|
//#region src/export-jobs.ts
|
|
2849
3020
|
const EXPORT_JOBS_VERSION = 1;
|
|
2850
3021
|
const MAX_EXPORT_JOBS = 100;
|
|
@@ -2870,6 +3041,7 @@ function normaliseJob(value) {
|
|
|
2870
3041
|
itemCount,
|
|
2871
3042
|
kind,
|
|
2872
3043
|
outputDir,
|
|
3044
|
+
scope: normaliseExportScope(record.scope),
|
|
2873
3045
|
startedAt,
|
|
2874
3046
|
status,
|
|
2875
3047
|
written
|
|
@@ -3080,10 +3252,16 @@ function transcriptCount(cacheData) {
|
|
|
3080
3252
|
return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
|
|
3081
3253
|
}
|
|
3082
3254
|
function cloneExportState(state) {
|
|
3083
|
-
return state ? {
|
|
3255
|
+
return state ? {
|
|
3256
|
+
...state,
|
|
3257
|
+
scope: cloneExportScope(state.scope)
|
|
3258
|
+
} : void 0;
|
|
3084
3259
|
}
|
|
3085
3260
|
function cloneExportJob(job) {
|
|
3086
|
-
return {
|
|
3261
|
+
return {
|
|
3262
|
+
...job,
|
|
3263
|
+
scope: cloneExportScope(job.scope)
|
|
3264
|
+
};
|
|
3087
3265
|
}
|
|
3088
3266
|
function cloneFolderSummary(folder) {
|
|
3089
3267
|
return { ...folder };
|
|
@@ -3334,7 +3512,7 @@ var GranolaApp = class {
|
|
|
3334
3512
|
this.emitStateUpdate();
|
|
3335
3513
|
return cloneExportJob(job);
|
|
3336
3514
|
}
|
|
3337
|
-
async startExportJob(kind, format, itemCount, outputDir) {
|
|
3515
|
+
async startExportJob(kind, format, itemCount, outputDir, scope) {
|
|
3338
3516
|
return await this.updateExportJob({
|
|
3339
3517
|
completedCount: 0,
|
|
3340
3518
|
format,
|
|
@@ -3342,6 +3520,7 @@ var GranolaApp = class {
|
|
|
3342
3520
|
itemCount,
|
|
3343
3521
|
kind,
|
|
3344
3522
|
outputDir,
|
|
3523
|
+
scope: cloneExportScope(scope),
|
|
3345
3524
|
startedAt: this.nowIso(),
|
|
3346
3525
|
status: "running",
|
|
3347
3526
|
written: 0
|
|
@@ -3611,25 +3790,29 @@ var GranolaApp = class {
|
|
|
3611
3790
|
this.setUiState({ view: "exports-history" });
|
|
3612
3791
|
return { jobs };
|
|
3613
3792
|
}
|
|
3614
|
-
async exportNotes(format = "markdown") {
|
|
3793
|
+
async exportNotes(format = "markdown", options = {}) {
|
|
3794
|
+
const documents = await this.listDocuments();
|
|
3795
|
+
const exportContext = await this.resolveExportContext(options.folderId);
|
|
3796
|
+
const filteredDocuments = exportContext.documentIds ? documents.filter((document) => exportContext.documentIds.has(document.id)) : documents;
|
|
3615
3797
|
return await this.runNotesExport({
|
|
3798
|
+
documents: filteredDocuments,
|
|
3616
3799
|
format,
|
|
3617
|
-
outputDir: this.config.notes.output
|
|
3800
|
+
outputDir: resolveExportOutputDir(options.outputDir ?? this.config.notes.output, exportContext.scope, { scopedDirectory: options.scopedOutput }),
|
|
3801
|
+
scope: exportContext.scope
|
|
3618
3802
|
});
|
|
3619
3803
|
}
|
|
3620
3804
|
async runNotesExport(options) {
|
|
3621
|
-
|
|
3622
|
-
let job = await this.startExportJob("notes", options.format, documents.length, options.outputDir);
|
|
3805
|
+
let job = await this.startExportJob("notes", options.format, options.documents.length, options.outputDir, options.scope);
|
|
3623
3806
|
let written = 0;
|
|
3624
3807
|
try {
|
|
3625
|
-
written = await writeNotes(documents, options.outputDir, options.format, { onProgress: async (progress) => {
|
|
3808
|
+
written = await writeNotes(options.documents, options.outputDir, options.format, { onProgress: async (progress) => {
|
|
3626
3809
|
job = await this.setExportJobProgress(job, {
|
|
3627
3810
|
completedCount: progress.completed,
|
|
3628
3811
|
written: progress.written
|
|
3629
3812
|
});
|
|
3630
3813
|
} });
|
|
3631
3814
|
job = await this.completeExportJob(job, {
|
|
3632
|
-
completedCount: documents.length,
|
|
3815
|
+
completedCount: options.documents.length,
|
|
3633
3816
|
written
|
|
3634
3817
|
});
|
|
3635
3818
|
} catch (error) {
|
|
@@ -3638,37 +3821,49 @@ var GranolaApp = class {
|
|
|
3638
3821
|
}
|
|
3639
3822
|
this.#state.exports.notes = {
|
|
3640
3823
|
format: options.format,
|
|
3641
|
-
itemCount: documents.length,
|
|
3824
|
+
itemCount: options.documents.length,
|
|
3642
3825
|
jobId: job.id,
|
|
3643
3826
|
outputDir: options.outputDir,
|
|
3644
3827
|
ranAt: this.nowIso(),
|
|
3828
|
+
scope: cloneExportScope(options.scope),
|
|
3645
3829
|
written
|
|
3646
3830
|
};
|
|
3647
3831
|
this.emitStateUpdate();
|
|
3648
|
-
this.setUiState({
|
|
3832
|
+
this.setUiState({
|
|
3833
|
+
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
3834
|
+
view: "notes-export"
|
|
3835
|
+
});
|
|
3649
3836
|
return {
|
|
3650
|
-
documentCount: documents.length,
|
|
3651
|
-
documents,
|
|
3837
|
+
documentCount: options.documents.length,
|
|
3838
|
+
documents: options.documents,
|
|
3652
3839
|
format: options.format,
|
|
3653
3840
|
job,
|
|
3654
3841
|
outputDir: options.outputDir,
|
|
3842
|
+
scope: cloneExportScope(options.scope),
|
|
3655
3843
|
written
|
|
3656
3844
|
};
|
|
3657
3845
|
}
|
|
3658
|
-
async exportTranscripts(format = "text") {
|
|
3846
|
+
async exportTranscripts(format = "text", options = {}) {
|
|
3847
|
+
const cacheData = await this.loadCache({ required: true });
|
|
3848
|
+
if (!cacheData) throw this.missingCacheError();
|
|
3849
|
+
const exportContext = await this.resolveExportContext(options.folderId);
|
|
3850
|
+
const scopedCacheData = exportContext.documentIds ? {
|
|
3851
|
+
documents: Object.fromEntries(Object.entries(cacheData.documents).filter(([id]) => exportContext.documentIds.has(id))),
|
|
3852
|
+
transcripts: Object.fromEntries(Object.entries(cacheData.transcripts).filter(([id]) => exportContext.documentIds.has(id)))
|
|
3853
|
+
} : cacheData;
|
|
3659
3854
|
return await this.runTranscriptsExport({
|
|
3855
|
+
cacheData: scopedCacheData,
|
|
3660
3856
|
format,
|
|
3661
|
-
outputDir: this.config.transcripts.output
|
|
3857
|
+
outputDir: resolveExportOutputDir(options.outputDir ?? this.config.transcripts.output, exportContext.scope, { scopedDirectory: options.scopedOutput }),
|
|
3858
|
+
scope: exportContext.scope
|
|
3662
3859
|
});
|
|
3663
3860
|
}
|
|
3664
3861
|
async runTranscriptsExport(options) {
|
|
3665
|
-
const
|
|
3666
|
-
|
|
3667
|
-
const count = transcriptCount(cacheData);
|
|
3668
|
-
let job = await this.startExportJob("transcripts", options.format, count, options.outputDir);
|
|
3862
|
+
const count = transcriptCount(options.cacheData);
|
|
3863
|
+
let job = await this.startExportJob("transcripts", options.format, count, options.outputDir, options.scope);
|
|
3669
3864
|
let written = 0;
|
|
3670
3865
|
try {
|
|
3671
|
-
written = await writeTranscripts(cacheData, options.outputDir, options.format, { onProgress: async (progress) => {
|
|
3866
|
+
written = await writeTranscripts(options.cacheData, options.outputDir, options.format, { onProgress: async (progress) => {
|
|
3672
3867
|
job = await this.setExportJobProgress(job, {
|
|
3673
3868
|
completedCount: progress.completed,
|
|
3674
3869
|
written: progress.written
|
|
@@ -3688,15 +3883,20 @@ var GranolaApp = class {
|
|
|
3688
3883
|
jobId: job.id,
|
|
3689
3884
|
outputDir: options.outputDir,
|
|
3690
3885
|
ranAt: this.nowIso(),
|
|
3886
|
+
scope: cloneExportScope(options.scope),
|
|
3691
3887
|
written
|
|
3692
3888
|
};
|
|
3693
3889
|
this.emitStateUpdate();
|
|
3694
|
-
this.setUiState({
|
|
3890
|
+
this.setUiState({
|
|
3891
|
+
selectedFolderId: options.scope.mode === "folder" ? options.scope.folderId : void 0,
|
|
3892
|
+
view: "transcripts-export"
|
|
3893
|
+
});
|
|
3695
3894
|
return {
|
|
3696
|
-
cacheData,
|
|
3895
|
+
cacheData: options.cacheData,
|
|
3697
3896
|
format: options.format,
|
|
3698
3897
|
job,
|
|
3699
3898
|
outputDir: options.outputDir,
|
|
3899
|
+
scope: cloneExportScope(options.scope),
|
|
3700
3900
|
transcriptCount: count,
|
|
3701
3901
|
written
|
|
3702
3902
|
};
|
|
@@ -3704,15 +3904,28 @@ var GranolaApp = class {
|
|
|
3704
3904
|
async rerunExportJob(id) {
|
|
3705
3905
|
const job = this.#state.exports.jobs.find((candidate) => candidate.id === id);
|
|
3706
3906
|
if (!job) throw new Error(`export job not found: ${id}`);
|
|
3707
|
-
if (job.kind === "notes") return await this.
|
|
3708
|
-
|
|
3709
|
-
outputDir: job.outputDir
|
|
3907
|
+
if (job.kind === "notes") return await this.exportNotes(job.format, {
|
|
3908
|
+
folderId: job.scope.mode === "folder" ? job.scope.folderId : void 0,
|
|
3909
|
+
outputDir: job.outputDir,
|
|
3910
|
+
scopedOutput: false
|
|
3710
3911
|
});
|
|
3711
|
-
return await this.
|
|
3712
|
-
|
|
3713
|
-
outputDir: job.outputDir
|
|
3912
|
+
return await this.exportTranscripts(job.format, {
|
|
3913
|
+
folderId: job.scope.mode === "folder" ? job.scope.folderId : void 0,
|
|
3914
|
+
outputDir: job.outputDir,
|
|
3915
|
+
scopedOutput: false
|
|
3714
3916
|
});
|
|
3715
3917
|
}
|
|
3918
|
+
async resolveExportContext(folderId) {
|
|
3919
|
+
if (!folderId) return { scope: allExportScope() };
|
|
3920
|
+
const folders = await this.loadFolders({ required: true });
|
|
3921
|
+
const summary = resolveFolder((folders ?? []).map((folder) => buildFolderSummary(folder)), folderId);
|
|
3922
|
+
const rawFolder = (folders ?? []).find((candidate) => candidate.id === summary.id);
|
|
3923
|
+
if (!rawFolder) throw new Error(`folder not found: ${folderId}`);
|
|
3924
|
+
return {
|
|
3925
|
+
documentIds: new Set(rawFolder.documentIds),
|
|
3926
|
+
scope: folderExportScope(summary)
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3716
3929
|
};
|
|
3717
3930
|
async function createGranolaApp(config, options = {}) {
|
|
3718
3931
|
const auth = await inspectDefaultGranolaAuth(config);
|
|
@@ -4003,8 +4216,8 @@ function renderExportJobs(jobs, format) {
|
|
|
4003
4216
|
if (format === "json") return toJson({ jobs });
|
|
4004
4217
|
if (format === "yaml") return toYaml({ jobs });
|
|
4005
4218
|
if (jobs.length === 0) return "No export jobs\n";
|
|
4006
|
-
return `${["ID KIND STATUS FORMAT ITEMS WRITTEN STARTED", ...jobs.map((job) => {
|
|
4007
|
-
return `${job.id.padEnd(28).slice(0, 28)} ${job.kind.padEnd(12)} ${job.status.padEnd(11)} ${job.format.padEnd(11)} ${String(job.itemCount).padEnd(7)} ${String(job.written).padEnd(8)} ${job.startedAt.slice(0, 19)}`;
|
|
4219
|
+
return `${["ID KIND STATUS FORMAT SCOPE ITEMS WRITTEN STARTED", ...jobs.map((job) => {
|
|
4220
|
+
return `${job.id.padEnd(28).slice(0, 28)} ${job.kind.padEnd(12)} ${job.status.padEnd(11)} ${job.format.padEnd(11)} ${renderExportScopeLabel(job.scope).padEnd(20).slice(0, 20)} ${String(job.itemCount).padEnd(7)} ${String(job.written).padEnd(8)} ${job.startedAt.slice(0, 19)}`;
|
|
4008
4221
|
})].join("\n")}\n`;
|
|
4009
4222
|
}
|
|
4010
4223
|
const exportsCommand = {
|
|
@@ -4054,10 +4267,10 @@ async function rerun(id, commandFlags, globalFlags) {
|
|
|
4054
4267
|
debug(config.debug, "cacheFile", config.transcripts.cacheFile || "(none)");
|
|
4055
4268
|
const result = await (await createGranolaApp(config)).rerunExportJob(id);
|
|
4056
4269
|
if ("documentCount" in result) {
|
|
4057
|
-
console.log(`✓ Reran notes export job ${result.job.id} to ${result.outputDir} (${result.written}/${result.documentCount} written)`);
|
|
4270
|
+
console.log(`✓ Reran notes export job ${result.job.id} from ${renderExportScopeLabel(result.scope)} to ${result.outputDir} (${result.written}/${result.documentCount} written)`);
|
|
4058
4271
|
return 0;
|
|
4059
4272
|
}
|
|
4060
|
-
console.log(`✓ Reran transcripts export job ${result.job.id} to ${result.outputDir} (${result.written}/${result.transcriptCount} written)`);
|
|
4273
|
+
console.log(`✓ Reran transcripts export job ${result.job.id} from ${renderExportScopeLabel(result.scope)} to ${result.outputDir} (${result.written}/${result.transcriptCount} written)`);
|
|
4061
4274
|
return 0;
|
|
4062
4275
|
}
|
|
4063
4276
|
//#endregion
|
|
@@ -4297,6 +4510,12 @@ function escapeHtml(value) {
|
|
|
4297
4510
|
.replaceAll('"', """);
|
|
4298
4511
|
}
|
|
4299
4512
|
|
|
4513
|
+
function exportScopeLabel(scope) {
|
|
4514
|
+
return scope && scope.mode === "folder"
|
|
4515
|
+
? "Folder: " + (scope.folderName || scope.folderId)
|
|
4516
|
+
: "Scope: All meetings";
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4300
4519
|
function setStatus(label, tone = "idle") {
|
|
4301
4520
|
els.stateBadge.textContent = label;
|
|
4302
4521
|
els.stateBadge.dataset.tone = tone;
|
|
@@ -4635,8 +4854,9 @@ function renderExportJobs() {
|
|
|
4635
4854
|
"</div>",
|
|
4636
4855
|
'<div class="job-card__status" data-status="' + escapeHtml(job.status) + '">' + escapeHtml(job.status) + "</div>",
|
|
4637
4856
|
"</div>",
|
|
4638
|
-
'<div class="job-card__meta">Format: ' + escapeHtml(job.format) + " • " + escapeHtml(progress) + " • Written: " + escapeHtml(String(job.written)) + "</div>",
|
|
4857
|
+
'<div class="job-card__meta">Format: ' + escapeHtml(job.format) + " • " + escapeHtml(exportScopeLabel(job.scope)) + " • " + escapeHtml(progress) + " • Written: " + escapeHtml(String(job.written)) + "</div>",
|
|
4639
4858
|
'<div class="job-card__meta">Started: ' + escapeHtml(job.startedAt.slice(0, 19)) + "</div>",
|
|
4859
|
+
'<div class="job-card__meta">Output: ' + escapeHtml(job.outputDir) + "</div>",
|
|
4640
4860
|
error,
|
|
4641
4861
|
'<div class="job-card__actions">' + rerunButton + "</div>",
|
|
4642
4862
|
"</article>",
|
|
@@ -4836,9 +5056,12 @@ async function syncAuthState() {
|
|
|
4836
5056
|
}
|
|
4837
5057
|
|
|
4838
5058
|
async function exportNotes() {
|
|
4839
|
-
setStatus("Exporting notes…", "busy");
|
|
5059
|
+
setStatus(state.selectedFolderId ? "Exporting folder notes…" : "Exporting notes…", "busy");
|
|
4840
5060
|
await fetchJson("/exports/notes", {
|
|
4841
|
-
body: JSON.stringify({
|
|
5061
|
+
body: JSON.stringify({
|
|
5062
|
+
folderId: state.selectedFolderId || undefined,
|
|
5063
|
+
format: "markdown",
|
|
5064
|
+
}),
|
|
4842
5065
|
headers: { "content-type": "application/json" },
|
|
4843
5066
|
method: "POST",
|
|
4844
5067
|
});
|
|
@@ -4846,9 +5069,15 @@ async function exportNotes() {
|
|
|
4846
5069
|
}
|
|
4847
5070
|
|
|
4848
5071
|
async function exportTranscripts() {
|
|
4849
|
-
setStatus(
|
|
5072
|
+
setStatus(
|
|
5073
|
+
state.selectedFolderId ? "Exporting folder transcripts…" : "Exporting transcripts…",
|
|
5074
|
+
"busy",
|
|
5075
|
+
);
|
|
4850
5076
|
await fetchJson("/exports/transcripts", {
|
|
4851
|
-
body: JSON.stringify({
|
|
5077
|
+
body: JSON.stringify({
|
|
5078
|
+
folderId: state.selectedFolderId || undefined,
|
|
5079
|
+
format: "text",
|
|
5080
|
+
}),
|
|
4852
5081
|
headers: { "content-type": "application/json" },
|
|
4853
5082
|
method: "POST",
|
|
4854
5083
|
});
|
|
@@ -5930,6 +6159,9 @@ function parseAuthMode(value) {
|
|
|
5930
6159
|
default: throw new Error("invalid auth mode: expected stored-session or supabase-file");
|
|
5931
6160
|
}
|
|
5932
6161
|
}
|
|
6162
|
+
function folderIdFromBody(value) {
|
|
6163
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
6164
|
+
}
|
|
5933
6165
|
function sendJson(response, body, init = {}) {
|
|
5934
6166
|
const payload = `${JSON.stringify(body, null, 2)}\n`;
|
|
5935
6167
|
response.writeHead(init.status ?? 200, {
|
|
@@ -6275,7 +6507,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6275
6507
|
}
|
|
6276
6508
|
if (method === "POST" && path === granolaTransportPaths.exportNotes) {
|
|
6277
6509
|
const body = await readJsonBody(request);
|
|
6278
|
-
sendJson(response, await app.exportNotes(noteFormatFromBody(body.format)), {
|
|
6510
|
+
sendJson(response, await app.exportNotes(noteFormatFromBody(body.format), { folderId: folderIdFromBody(body.folderId) }), {
|
|
6279
6511
|
headers: originHeaders,
|
|
6280
6512
|
status: 202
|
|
6281
6513
|
});
|
|
@@ -6297,7 +6529,7 @@ async function startGranolaServer(app, options = {}) {
|
|
|
6297
6529
|
}
|
|
6298
6530
|
if (method === "POST" && path === granolaTransportPaths.exportTranscripts) {
|
|
6299
6531
|
const body = await readJsonBody(request);
|
|
6300
|
-
sendJson(response, await app.exportTranscripts(transcriptFormatFromBody(body.format)), {
|
|
6532
|
+
sendJson(response, await app.exportTranscripts(transcriptFormatFromBody(body.format), { folderId: folderIdFromBody(body.folderId) }), {
|
|
6301
6533
|
headers: originHeaders,
|
|
6302
6534
|
status: 202
|
|
6303
6535
|
});
|
|
@@ -6677,6 +6909,7 @@ Usage:
|
|
|
6677
6909
|
granola notes [options]
|
|
6678
6910
|
|
|
6679
6911
|
Options:
|
|
6912
|
+
--folder <query> Export only meetings inside one folder id or name
|
|
6680
6913
|
--format <value> Output format: markdown, json, yaml, raw (default: markdown)
|
|
6681
6914
|
--output <path> Output directory for note files (default: ./notes)
|
|
6682
6915
|
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
@@ -6689,6 +6922,7 @@ Options:
|
|
|
6689
6922
|
const notesCommand = {
|
|
6690
6923
|
description: "Export Granola notes",
|
|
6691
6924
|
flags: {
|
|
6925
|
+
folder: { type: "string" },
|
|
6692
6926
|
format: { type: "string" },
|
|
6693
6927
|
help: { type: "boolean" },
|
|
6694
6928
|
output: { type: "string" },
|
|
@@ -6709,8 +6943,14 @@ const notesCommand = {
|
|
|
6709
6943
|
debug(config.debug, "format", format);
|
|
6710
6944
|
const app = await createGranolaApp(config);
|
|
6711
6945
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
6712
|
-
const
|
|
6713
|
-
|
|
6946
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
6947
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
6948
|
+
debug(config.debug, "folder", folder?.id ?? "(all)");
|
|
6949
|
+
const result = await app.exportNotes(format, {
|
|
6950
|
+
folderId: folder?.id,
|
|
6951
|
+
scopedOutput: typeof commandFlags.output !== "string"
|
|
6952
|
+
});
|
|
6953
|
+
console.log(`✓ Exported ${result.documentCount} notes from ${renderExportScopeLabel(result.scope)} to ${result.outputDir} (job ${result.job.id})`);
|
|
6714
6954
|
debug(config.debug, "notes written", result.written);
|
|
6715
6955
|
return 0;
|
|
6716
6956
|
}
|
|
@@ -6863,6 +7103,7 @@ Usage:
|
|
|
6863
7103
|
|
|
6864
7104
|
Options:
|
|
6865
7105
|
--cache <path> Path to Granola cache JSON
|
|
7106
|
+
--folder <query> Export only meetings inside one folder id or name
|
|
6866
7107
|
--format <value> Output format: text, json, yaml, raw (default: text)
|
|
6867
7108
|
--output <path> Output directory for transcript files (default: ./transcripts)
|
|
6868
7109
|
--debug Enable debug logging
|
|
@@ -6874,6 +7115,7 @@ const transcriptsCommand = {
|
|
|
6874
7115
|
description: "Export Granola transcripts",
|
|
6875
7116
|
flags: {
|
|
6876
7117
|
cache: { type: "string" },
|
|
7118
|
+
folder: { type: "string" },
|
|
6877
7119
|
format: { type: "string" },
|
|
6878
7120
|
help: { type: "boolean" },
|
|
6879
7121
|
output: { type: "string" }
|
|
@@ -6892,8 +7134,14 @@ const transcriptsCommand = {
|
|
|
6892
7134
|
debug(config.debug, "format", format);
|
|
6893
7135
|
const app = await createGranolaApp(config);
|
|
6894
7136
|
debug(config.debug, "authMode", app.getState().auth.mode);
|
|
6895
|
-
const
|
|
6896
|
-
|
|
7137
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
7138
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
7139
|
+
debug(config.debug, "folder", folder?.id ?? "(all)");
|
|
7140
|
+
const result = await app.exportTranscripts(format, {
|
|
7141
|
+
folderId: folder?.id,
|
|
7142
|
+
scopedOutput: typeof commandFlags.output !== "string"
|
|
7143
|
+
});
|
|
7144
|
+
console.log(`✓ Exported ${result.transcriptCount} transcripts from ${renderExportScopeLabel(result.scope)} to ${result.outputDir} (job ${result.job.id})`);
|
|
6897
7145
|
debug(config.debug, "transcripts written", result.written);
|
|
6898
7146
|
return 0;
|
|
6899
7147
|
}
|