granola-toolkit 0.42.0 → 0.44.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 +5 -0
- package/dist/cli.js +264 -14
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ npx granola-toolkit --help
|
|
|
17
17
|
|
|
18
18
|
The published package exposes both `granola` and `granola-toolkit` as executable names.
|
|
19
19
|
|
|
20
|
+
If you do not want to install via npm, each GitHub release also publishes standalone archives for
|
|
21
|
+
macOS arm64, Linux x64, and Windows x64. Extract the archive and run `granola` (or
|
|
22
|
+
`granola.exe` on Windows).
|
|
23
|
+
|
|
20
24
|
## Quick Start
|
|
21
25
|
|
|
22
26
|
```bash
|
|
@@ -25,6 +29,7 @@ granola sync
|
|
|
25
29
|
granola sync --watch
|
|
26
30
|
granola automation rules
|
|
27
31
|
granola automation runs
|
|
32
|
+
granola search customer onboarding
|
|
28
33
|
granola folder list
|
|
29
34
|
granola meeting list --limit 10
|
|
30
35
|
granola notes --folder Team
|
package/dist/cli.js
CHANGED
|
@@ -2626,6 +2626,7 @@ function defaultGranolaToolkitPersistenceLayout(options = {}) {
|
|
|
2626
2626
|
dataDirectory,
|
|
2627
2627
|
exportJobsFile: join(dataDirectory, "export-jobs.json"),
|
|
2628
2628
|
meetingIndexFile: join(dataDirectory, "meeting-index.json"),
|
|
2629
|
+
searchIndexFile: join(dataDirectory, "search-index.json"),
|
|
2629
2630
|
sessionFile: join(dataDirectory, "session.json"),
|
|
2630
2631
|
sessionStoreKind: targetPlatform === "darwin" ? "keychain" : "file",
|
|
2631
2632
|
syncEventsFile: join(dataDirectory, "sync-events.jsonl"),
|
|
@@ -4352,6 +4353,128 @@ function buildSyncEvents(runId, occurredAt, changes, previousMeetings, nextMeeti
|
|
|
4352
4353
|
}));
|
|
4353
4354
|
}
|
|
4354
4355
|
//#endregion
|
|
4356
|
+
//#region src/search-index.ts
|
|
4357
|
+
const SEARCH_INDEX_VERSION = 1;
|
|
4358
|
+
function cloneEntry(entry) {
|
|
4359
|
+
return {
|
|
4360
|
+
...entry,
|
|
4361
|
+
folderIds: [...entry.folderIds],
|
|
4362
|
+
folderNames: [...entry.folderNames],
|
|
4363
|
+
tags: [...entry.tags]
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
function noteText(document) {
|
|
4367
|
+
const notes = document.notesPlain.trim();
|
|
4368
|
+
if (notes) return notes;
|
|
4369
|
+
const panel = document.lastViewedPanel?.originalContent?.trim();
|
|
4370
|
+
if (panel) return panel;
|
|
4371
|
+
return document.content.trim();
|
|
4372
|
+
}
|
|
4373
|
+
function transcriptText(documentId, cacheData) {
|
|
4374
|
+
return (cacheData?.transcripts[documentId] ?? []).filter((segment) => segment.isFinal).map((segment) => segment.text.trim()).filter(Boolean).join("\n");
|
|
4375
|
+
}
|
|
4376
|
+
function buildSearchIndex(documents, options = {}) {
|
|
4377
|
+
return documents.map((document) => {
|
|
4378
|
+
const folders = options.foldersByDocumentId?.get(document.id) ?? [];
|
|
4379
|
+
const transcript = transcriptText(document.id, options.cacheData);
|
|
4380
|
+
return {
|
|
4381
|
+
createdAt: document.createdAt,
|
|
4382
|
+
folderIds: folders.map((folder) => folder.id),
|
|
4383
|
+
folderNames: folders.map((folder) => folder.name || folder.id),
|
|
4384
|
+
id: document.id,
|
|
4385
|
+
noteText: noteText(document),
|
|
4386
|
+
tags: [...document.tags],
|
|
4387
|
+
title: document.title,
|
|
4388
|
+
transcriptLoaded: transcript.length > 0,
|
|
4389
|
+
transcriptText: transcript,
|
|
4390
|
+
updatedAt: document.updatedAt
|
|
4391
|
+
};
|
|
4392
|
+
}).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
4393
|
+
}
|
|
4394
|
+
function searchFieldScore(value, term) {
|
|
4395
|
+
const lower = value.toLowerCase();
|
|
4396
|
+
if (!lower || !term) return 0;
|
|
4397
|
+
if (lower === term) return 8;
|
|
4398
|
+
if (lower.startsWith(term)) return 5;
|
|
4399
|
+
if (lower.includes(term)) return 3;
|
|
4400
|
+
return 0;
|
|
4401
|
+
}
|
|
4402
|
+
function combinedText(entry) {
|
|
4403
|
+
return [
|
|
4404
|
+
entry.id,
|
|
4405
|
+
entry.title,
|
|
4406
|
+
...entry.tags,
|
|
4407
|
+
...entry.folderNames,
|
|
4408
|
+
entry.noteText,
|
|
4409
|
+
entry.transcriptText
|
|
4410
|
+
].join("\n").toLowerCase();
|
|
4411
|
+
}
|
|
4412
|
+
function searchEntryScore(entry, term) {
|
|
4413
|
+
const scoredFields = [
|
|
4414
|
+
searchFieldScore(entry.id, term) * 5,
|
|
4415
|
+
searchFieldScore(entry.title, term) * 8,
|
|
4416
|
+
...entry.tags.map((tag) => searchFieldScore(tag, term) * 6),
|
|
4417
|
+
...entry.folderNames.map((folderName) => searchFieldScore(folderName, term) * 4)
|
|
4418
|
+
].filter((score) => score > 0);
|
|
4419
|
+
if (scoredFields.length > 0) return Math.max(...scoredFields);
|
|
4420
|
+
if (combinedText(entry).includes(term)) return 1;
|
|
4421
|
+
}
|
|
4422
|
+
function searchSearchIndex(entries, query) {
|
|
4423
|
+
const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
4424
|
+
if (terms.length === 0) return [];
|
|
4425
|
+
return entries.map((entry) => {
|
|
4426
|
+
let score = 0;
|
|
4427
|
+
for (const term of terms) {
|
|
4428
|
+
const termScore = searchEntryScore(entry, term);
|
|
4429
|
+
if (termScore == null) return;
|
|
4430
|
+
score += termScore;
|
|
4431
|
+
}
|
|
4432
|
+
return {
|
|
4433
|
+
id: entry.id,
|
|
4434
|
+
score,
|
|
4435
|
+
updatedAt: entry.updatedAt
|
|
4436
|
+
};
|
|
4437
|
+
}).filter((entry) => Boolean(entry)).sort((left, right) => right.score - left.score || right.updatedAt.localeCompare(left.updatedAt) || left.id.localeCompare(right.id)).map(({ id, score }) => ({
|
|
4438
|
+
id,
|
|
4439
|
+
score
|
|
4440
|
+
}));
|
|
4441
|
+
}
|
|
4442
|
+
var FileSearchIndexStore = class {
|
|
4443
|
+
constructor(filePath = defaultSearchIndexFilePath()) {
|
|
4444
|
+
this.filePath = filePath;
|
|
4445
|
+
}
|
|
4446
|
+
async readIndex() {
|
|
4447
|
+
try {
|
|
4448
|
+
const parsed = parseJsonString(await readFile(this.filePath, "utf8"));
|
|
4449
|
+
if (!parsed || parsed.version !== SEARCH_INDEX_VERSION || !Array.isArray(parsed.entries)) return [];
|
|
4450
|
+
return parsed.entries.map(cloneEntry);
|
|
4451
|
+
} catch {
|
|
4452
|
+
return [];
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
async writeIndex(entries) {
|
|
4456
|
+
await mkdir(dirname(this.filePath), { recursive: true });
|
|
4457
|
+
const payload = {
|
|
4458
|
+
entries: entries.map(cloneEntry),
|
|
4459
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4460
|
+
version: SEARCH_INDEX_VERSION
|
|
4461
|
+
};
|
|
4462
|
+
await writeFile(this.filePath, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
4463
|
+
encoding: "utf8",
|
|
4464
|
+
mode: 384
|
|
4465
|
+
});
|
|
4466
|
+
}
|
|
4467
|
+
};
|
|
4468
|
+
function defaultSearchIndexFilePath() {
|
|
4469
|
+
return defaultGranolaToolkitPersistenceLayout().searchIndexFile;
|
|
4470
|
+
}
|
|
4471
|
+
function createDefaultSearchIndexStore() {
|
|
4472
|
+
return new FileSearchIndexStore();
|
|
4473
|
+
}
|
|
4474
|
+
function meetingIdsFromSearchResults(results) {
|
|
4475
|
+
return results.map((result) => result.id);
|
|
4476
|
+
}
|
|
4477
|
+
//#endregion
|
|
4355
4478
|
//#region src/app/core.ts
|
|
4356
4479
|
function transcriptCount(cacheData) {
|
|
4357
4480
|
return Object.values(cacheData.transcripts).filter((segments) => segments.length > 0).length;
|
|
@@ -4477,6 +4600,7 @@ var GranolaApp = class {
|
|
|
4477
4600
|
#granolaClient;
|
|
4478
4601
|
#documents;
|
|
4479
4602
|
#meetingIndex;
|
|
4603
|
+
#searchIndex;
|
|
4480
4604
|
#listeners = /* @__PURE__ */ new Set();
|
|
4481
4605
|
#refreshingMeetingIndex;
|
|
4482
4606
|
#state;
|
|
@@ -4505,6 +4629,12 @@ var GranolaApp = class {
|
|
|
4505
4629
|
runsFile: defaultAutomationRunsFilePath()
|
|
4506
4630
|
};
|
|
4507
4631
|
this.#meetingIndex = (deps.meetingIndex ?? []).map((meeting) => cloneMeetingSummary(meeting));
|
|
4632
|
+
this.#searchIndex = (deps.searchIndex ?? []).map((entry) => ({
|
|
4633
|
+
...entry,
|
|
4634
|
+
folderIds: [...entry.folderIds],
|
|
4635
|
+
folderNames: [...entry.folderNames],
|
|
4636
|
+
tags: [...entry.tags]
|
|
4637
|
+
}));
|
|
4508
4638
|
this.#state.index = {
|
|
4509
4639
|
available: this.#meetingIndex.length > 0,
|
|
4510
4640
|
filePath: defaultMeetingIndexFilePath(),
|
|
@@ -4688,6 +4818,15 @@ var GranolaApp = class {
|
|
|
4688
4818
|
if (this.deps.meetingIndexStore) await this.deps.meetingIndexStore.writeIndex(this.#meetingIndex);
|
|
4689
4819
|
this.emitStateUpdate();
|
|
4690
4820
|
}
|
|
4821
|
+
async persistSearchIndex(entries) {
|
|
4822
|
+
this.#searchIndex = entries.map((entry) => ({
|
|
4823
|
+
...entry,
|
|
4824
|
+
folderIds: [...entry.folderIds],
|
|
4825
|
+
folderNames: [...entry.folderNames],
|
|
4826
|
+
tags: [...entry.tags]
|
|
4827
|
+
}));
|
|
4828
|
+
if (this.deps.searchIndexStore) await this.deps.searchIndexStore.writeIndex(this.#searchIndex);
|
|
4829
|
+
}
|
|
4691
4830
|
async liveMeetingSnapshot(options = {}) {
|
|
4692
4831
|
const cacheData = await this.loadCache({ forceRefresh: options.forceRefresh });
|
|
4693
4832
|
const documents = await this.listDocuments({ forceRefresh: options.forceRefresh });
|
|
@@ -5155,6 +5294,10 @@ var GranolaApp = class {
|
|
|
5155
5294
|
try {
|
|
5156
5295
|
const snapshot = await this.liveMeetingSnapshot({ forceRefresh: options.forceRefresh ?? true });
|
|
5157
5296
|
await this.persistMeetingIndex(snapshot.meetings);
|
|
5297
|
+
await this.persistSearchIndex(buildSearchIndex(snapshot.documents, {
|
|
5298
|
+
cacheData: snapshot.cacheData,
|
|
5299
|
+
foldersByDocumentId: this.buildFoldersByDocumentId(snapshot.folders)
|
|
5300
|
+
}));
|
|
5158
5301
|
const { changes, summary } = diffMeetingSummaries(previousMeetings, snapshot.meetings, snapshot.folders?.length ?? 0);
|
|
5159
5302
|
const completedAt = this.nowIso();
|
|
5160
5303
|
const runId = this.createSyncRunId();
|
|
@@ -5285,10 +5428,34 @@ var GranolaApp = class {
|
|
|
5285
5428
|
const summary = resolveFolderQuery((await this.loadFolders({ required: true }) ?? []).map((folder) => buildFolderSummary(folder)), query);
|
|
5286
5429
|
return await this.getFolder(summary.id);
|
|
5287
5430
|
}
|
|
5431
|
+
indexedMeetingsForSearch(options) {
|
|
5432
|
+
const rankedIds = meetingIdsFromSearchResults(searchSearchIndex(this.#searchIndex, options.search));
|
|
5433
|
+
const rankById = new Map(rankedIds.map((id, index) => [id, index]));
|
|
5434
|
+
return filterMeetingSummaries([...this.#meetingIndex.filter((meeting) => rankById.has(meeting.id))].sort((left, right) => {
|
|
5435
|
+
const leftRank = rankById.get(left.id) ?? Number.MAX_SAFE_INTEGER;
|
|
5436
|
+
const rightRank = rankById.get(right.id) ?? Number.MAX_SAFE_INTEGER;
|
|
5437
|
+
if (leftRank !== rightRank) return leftRank - rightRank;
|
|
5438
|
+
return right.updatedAt.localeCompare(left.updatedAt);
|
|
5439
|
+
}), {
|
|
5440
|
+
folderId: options.folderId,
|
|
5441
|
+
limit: options.limit,
|
|
5442
|
+
sort: options.sort,
|
|
5443
|
+
updatedFrom: options.updatedFrom,
|
|
5444
|
+
updatedTo: options.updatedTo
|
|
5445
|
+
});
|
|
5446
|
+
}
|
|
5288
5447
|
async listMeetings(options = {}) {
|
|
5289
5448
|
const preferIndex = options.preferIndex ?? (this.#state.ui.surface === "web" || this.#state.ui.surface === "server");
|
|
5290
|
-
|
|
5291
|
-
|
|
5449
|
+
const canUseSearchIndex = Boolean(options.search?.trim()) && !options.forceRefresh && this.#searchIndex.length > 0;
|
|
5450
|
+
if (!options.forceRefresh && preferIndex && this.#meetingIndex.length > 0 && (canUseSearchIndex || !this.#documents)) {
|
|
5451
|
+
const meetings = canUseSearchIndex ? this.indexedMeetingsForSearch({
|
|
5452
|
+
folderId: options.folderId,
|
|
5453
|
+
limit: options.limit,
|
|
5454
|
+
search: options.search,
|
|
5455
|
+
sort: options.sort,
|
|
5456
|
+
updatedFrom: options.updatedFrom,
|
|
5457
|
+
updatedTo: options.updatedTo
|
|
5458
|
+
}) : filterMeetingSummaries(this.#meetingIndex, options);
|
|
5292
5459
|
this.setUiState({
|
|
5293
5460
|
folderSearch: void 0,
|
|
5294
5461
|
meetingListSource: "index",
|
|
@@ -5367,7 +5534,14 @@ var GranolaApp = class {
|
|
|
5367
5534
|
return bundle;
|
|
5368
5535
|
}
|
|
5369
5536
|
async findMeeting(query, options = {}) {
|
|
5370
|
-
|
|
5537
|
+
let bundle;
|
|
5538
|
+
try {
|
|
5539
|
+
bundle = await this.readMeetingBundleByQuery(query, options);
|
|
5540
|
+
} catch (error) {
|
|
5541
|
+
const fallbackId = meetingIdsFromSearchResults(searchSearchIndex(this.#searchIndex, query))[0];
|
|
5542
|
+
if (!fallbackId) throw error;
|
|
5543
|
+
bundle = await this.readMeetingBundleById(fallbackId, options);
|
|
5544
|
+
}
|
|
5371
5545
|
this.setUiState({
|
|
5372
5546
|
selectedFolderId: bundle.meeting.meeting.folders[0]?.id,
|
|
5373
5547
|
selectedMeetingId: bundle.document.id,
|
|
@@ -5531,6 +5705,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
5531
5705
|
const exportJobs = await exportJobStore.readJobs();
|
|
5532
5706
|
const meetingIndexStore = createDefaultMeetingIndexStore();
|
|
5533
5707
|
const meetingIndex = await meetingIndexStore.readIndex();
|
|
5708
|
+
const searchIndexStore = createDefaultSearchIndexStore();
|
|
5709
|
+
const searchIndex = await searchIndexStore.readIndex();
|
|
5534
5710
|
const syncEventStore = createDefaultSyncEventStore();
|
|
5535
5711
|
const syncStateStore = createDefaultSyncStateStore();
|
|
5536
5712
|
const syncState = await syncStateStore.readState();
|
|
@@ -5550,6 +5726,8 @@ async function createGranolaApp(config, options = {}) {
|
|
|
5550
5726
|
meetingIndex,
|
|
5551
5727
|
meetingIndexStore,
|
|
5552
5728
|
now: options.now,
|
|
5729
|
+
searchIndex,
|
|
5730
|
+
searchIndexStore,
|
|
5553
5731
|
syncEventStore,
|
|
5554
5732
|
syncState,
|
|
5555
5733
|
syncStateStore
|
|
@@ -5724,7 +5902,7 @@ Options:
|
|
|
5724
5902
|
-h, --help Show help
|
|
5725
5903
|
`;
|
|
5726
5904
|
}
|
|
5727
|
-
function resolveFormat(value) {
|
|
5905
|
+
function resolveFormat$1(value) {
|
|
5728
5906
|
switch (value) {
|
|
5729
5907
|
case void 0: return "text";
|
|
5730
5908
|
case "json":
|
|
@@ -5733,7 +5911,7 @@ function resolveFormat(value) {
|
|
|
5733
5911
|
default: throw new Error("invalid automation format: expected text, json, or yaml");
|
|
5734
5912
|
}
|
|
5735
5913
|
}
|
|
5736
|
-
function parseLimit$
|
|
5914
|
+
function parseLimit$4(value) {
|
|
5737
5915
|
if (value === void 0) return 20;
|
|
5738
5916
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid automation limit: expected a positive integer");
|
|
5739
5917
|
return Number(value);
|
|
@@ -5793,7 +5971,7 @@ const automationCommand = {
|
|
|
5793
5971
|
name: "automation",
|
|
5794
5972
|
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
5795
5973
|
const [action] = commandArgs;
|
|
5796
|
-
const format = resolveFormat(commandFlags.format);
|
|
5974
|
+
const format = resolveFormat$1(commandFlags.format);
|
|
5797
5975
|
const config = await loadConfig({
|
|
5798
5976
|
globalFlags,
|
|
5799
5977
|
subcommandFlags: commandFlags
|
|
@@ -5808,13 +5986,13 @@ const automationCommand = {
|
|
|
5808
5986
|
return 0;
|
|
5809
5987
|
}
|
|
5810
5988
|
case "matches": {
|
|
5811
|
-
const result = await app.listAutomationMatches({ limit: parseLimit$
|
|
5989
|
+
const result = await app.listAutomationMatches({ limit: parseLimit$4(commandFlags.limit) });
|
|
5812
5990
|
console.log(renderMatches(result.matches, format).trimEnd());
|
|
5813
5991
|
return 0;
|
|
5814
5992
|
}
|
|
5815
5993
|
case "runs": {
|
|
5816
5994
|
const result = await app.listAutomationRuns({
|
|
5817
|
-
limit: parseLimit$
|
|
5995
|
+
limit: parseLimit$4(commandFlags.limit),
|
|
5818
5996
|
status: parseRunStatus(commandFlags.status)
|
|
5819
5997
|
});
|
|
5820
5998
|
console.log(renderRuns(result.runs, format).trimEnd());
|
|
@@ -5976,7 +6154,7 @@ function resolveListFormat$1(value) {
|
|
|
5976
6154
|
default: throw new Error("invalid exports format: expected text, json, or yaml");
|
|
5977
6155
|
}
|
|
5978
6156
|
}
|
|
5979
|
-
function parseLimit$
|
|
6157
|
+
function parseLimit$3(value) {
|
|
5980
6158
|
if (value === void 0) return 20;
|
|
5981
6159
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid exports limit: expected a positive integer");
|
|
5982
6160
|
const limit = Number(value);
|
|
@@ -6018,7 +6196,7 @@ const exportsCommand = {
|
|
|
6018
6196
|
};
|
|
6019
6197
|
async function list$2(commandFlags, globalFlags) {
|
|
6020
6198
|
const format = resolveListFormat$1(commandFlags.format);
|
|
6021
|
-
const limit = parseLimit$
|
|
6199
|
+
const limit = parseLimit$3(commandFlags.limit);
|
|
6022
6200
|
const config = await loadConfig({
|
|
6023
6201
|
globalFlags,
|
|
6024
6202
|
subcommandFlags: commandFlags
|
|
@@ -6079,7 +6257,7 @@ function resolveFolderListFormat(value) {
|
|
|
6079
6257
|
function resolveFolderDetailFormat(value) {
|
|
6080
6258
|
return resolveFolderListFormat(value);
|
|
6081
6259
|
}
|
|
6082
|
-
function parseLimit$
|
|
6260
|
+
function parseLimit$2(value) {
|
|
6083
6261
|
if (value === void 0) return 20;
|
|
6084
6262
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid folder limit: expected a positive integer");
|
|
6085
6263
|
const limit = Number(value);
|
|
@@ -6113,7 +6291,7 @@ const folderCommand = {
|
|
|
6113
6291
|
};
|
|
6114
6292
|
async function list$1(commandFlags, globalFlags) {
|
|
6115
6293
|
const format = resolveFolderListFormat(commandFlags.format);
|
|
6116
|
-
const limit = parseLimit$
|
|
6294
|
+
const limit = parseLimit$2(commandFlags.limit);
|
|
6117
6295
|
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
6118
6296
|
const config = await loadConfig({
|
|
6119
6297
|
globalFlags,
|
|
@@ -10182,7 +10360,7 @@ function resolveTranscriptFormat$1(value) {
|
|
|
10182
10360
|
default: throw new Error("invalid meeting transcript format: expected text, json, yaml, or raw");
|
|
10183
10361
|
}
|
|
10184
10362
|
}
|
|
10185
|
-
function parseLimit(value) {
|
|
10363
|
+
function parseLimit$1(value) {
|
|
10186
10364
|
if (value === void 0) return 20;
|
|
10187
10365
|
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid meeting limit: expected a positive integer");
|
|
10188
10366
|
const limit = Number(value);
|
|
@@ -10236,7 +10414,7 @@ const meetingCommand = {
|
|
|
10236
10414
|
};
|
|
10237
10415
|
async function list(commandFlags, globalFlags) {
|
|
10238
10416
|
const format = resolveListFormat(commandFlags.format);
|
|
10239
|
-
const limit = parseLimit(commandFlags.limit);
|
|
10417
|
+
const limit = parseLimit$1(commandFlags.limit);
|
|
10240
10418
|
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
10241
10419
|
const search = typeof commandFlags.search === "string" ? commandFlags.search : void 0;
|
|
10242
10420
|
const config = await loadConfig({
|
|
@@ -10417,6 +10595,77 @@ function resolveNoteFormat(value) {
|
|
|
10417
10595
|
}
|
|
10418
10596
|
}
|
|
10419
10597
|
//#endregion
|
|
10598
|
+
//#region src/commands/search.ts
|
|
10599
|
+
function searchHelp() {
|
|
10600
|
+
return `Granola search
|
|
10601
|
+
|
|
10602
|
+
Usage:
|
|
10603
|
+
granola search <query> [options]
|
|
10604
|
+
|
|
10605
|
+
Options:
|
|
10606
|
+
--folder <query> Filter search results to one folder id or name
|
|
10607
|
+
--format <value> text, json, yaml (default: text)
|
|
10608
|
+
--limit <n> Number of meetings to show (default: 20)
|
|
10609
|
+
--timeout <value> Request timeout, e.g. 2m, 30s, 120000 (default: 2m)
|
|
10610
|
+
--supabase <path> Path to supabase.json
|
|
10611
|
+
--debug Enable debug logging
|
|
10612
|
+
--config <path> Path to .granola.toml
|
|
10613
|
+
-h, --help Show help
|
|
10614
|
+
`;
|
|
10615
|
+
}
|
|
10616
|
+
function resolveFormat(value) {
|
|
10617
|
+
switch (value) {
|
|
10618
|
+
case void 0: return "text";
|
|
10619
|
+
case "json":
|
|
10620
|
+
case "text":
|
|
10621
|
+
case "yaml": return value;
|
|
10622
|
+
default: throw new Error("invalid search format: expected text, json, or yaml");
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10625
|
+
function parseLimit(value) {
|
|
10626
|
+
if (value === void 0) return 20;
|
|
10627
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) throw new Error("invalid search limit: expected a positive integer");
|
|
10628
|
+
return Number(value);
|
|
10629
|
+
}
|
|
10630
|
+
const searchCommand = {
|
|
10631
|
+
description: "Search meetings across titles, notes, transcripts, folders, and tags",
|
|
10632
|
+
flags: {
|
|
10633
|
+
folder: { type: "string" },
|
|
10634
|
+
format: { type: "string" },
|
|
10635
|
+
help: { type: "boolean" },
|
|
10636
|
+
limit: { type: "string" },
|
|
10637
|
+
timeout: { type: "string" }
|
|
10638
|
+
},
|
|
10639
|
+
help: searchHelp,
|
|
10640
|
+
name: "search",
|
|
10641
|
+
async run({ commandArgs, commandFlags, globalFlags }) {
|
|
10642
|
+
const query = commandArgs.join(" ").trim();
|
|
10643
|
+
if (!query) {
|
|
10644
|
+
console.log(searchHelp());
|
|
10645
|
+
return 1;
|
|
10646
|
+
}
|
|
10647
|
+
const format = resolveFormat(commandFlags.format);
|
|
10648
|
+
const limit = parseLimit(commandFlags.limit);
|
|
10649
|
+
const folderQuery = typeof commandFlags.folder === "string" ? commandFlags.folder : void 0;
|
|
10650
|
+
const config = await loadConfig({
|
|
10651
|
+
globalFlags,
|
|
10652
|
+
subcommandFlags: commandFlags
|
|
10653
|
+
});
|
|
10654
|
+
debug(config.debug, "using config", config.configFileUsed ?? "(none)");
|
|
10655
|
+
const app = await createGranolaApp(config);
|
|
10656
|
+
const folder = folderQuery ? await app.findFolder(folderQuery) : void 0;
|
|
10657
|
+
const result = await app.listMeetings({
|
|
10658
|
+
folderId: folder?.id,
|
|
10659
|
+
limit,
|
|
10660
|
+
preferIndex: true,
|
|
10661
|
+
search: query
|
|
10662
|
+
});
|
|
10663
|
+
console.log(result.source === "index" ? "Searched the local index" : "Search index unavailable, fell back to live meeting metadata");
|
|
10664
|
+
console.log(renderMeetingList(result.meetings, format).trimEnd());
|
|
10665
|
+
return 0;
|
|
10666
|
+
}
|
|
10667
|
+
};
|
|
10668
|
+
//#endregion
|
|
10420
10669
|
//#region src/commands/serve.ts
|
|
10421
10670
|
function serveHelp() {
|
|
10422
10671
|
return `Granola serve
|
|
@@ -10771,6 +11020,7 @@ const commands = [
|
|
|
10771
11020
|
folderCommand,
|
|
10772
11021
|
meetingCommand,
|
|
10773
11022
|
notesCommand,
|
|
11023
|
+
searchCommand,
|
|
10774
11024
|
serveCommand,
|
|
10775
11025
|
syncCommand,
|
|
10776
11026
|
tuiCommand,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granola-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Toolkit for exporting and working with Granola meetings, notes, and transcripts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"release:major": "node scripts/release.mjs major",
|
|
49
49
|
"release:minor": "node scripts/release.mjs minor",
|
|
50
50
|
"release:patch": "node scripts/release.mjs patch",
|
|
51
|
+
"standalone:build": "node scripts/build-standalone.mjs",
|
|
52
|
+
"standalone:smoke": "node scripts/build-standalone.mjs --smoke-test",
|
|
51
53
|
"start": "node dist/cli.js",
|
|
52
54
|
"notes": "node dist/cli.js notes",
|
|
53
55
|
"tui": "node dist/cli.js tui",
|
|
@@ -66,6 +68,8 @@
|
|
|
66
68
|
"devDependencies": {
|
|
67
69
|
"@types/node": "^25.5.2",
|
|
68
70
|
"@vitest/coverage-v8": "4.1.2",
|
|
71
|
+
"esbuild": "^0.27.7",
|
|
72
|
+
"postject": "^1.0.0-alpha.6",
|
|
69
73
|
"typescript": "^5.9.3",
|
|
70
74
|
"vite-plugin-solid": "^2.11.11",
|
|
71
75
|
"vite-plus": "0.1.15"
|