granola-cli 0.1.1 → 0.2.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 +57 -5
- package/dist/main.js +348 -87
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -254,12 +254,12 @@ async function isLockStale(lockPath) {
|
|
|
254
254
|
return true;
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
|
-
async function acquireLock() {
|
|
257
|
+
async function acquireLock(timeoutMs = LOCK_TIMEOUT_MS) {
|
|
258
258
|
const lockPath = getLockFilePath();
|
|
259
259
|
const startTime = Date.now();
|
|
260
260
|
await ensureLockDirectory();
|
|
261
261
|
debug4("attempting to acquire lock at %s", lockPath);
|
|
262
|
-
while (Date.now() - startTime <
|
|
262
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
263
263
|
try {
|
|
264
264
|
const handle = await open(lockPath, "wx");
|
|
265
265
|
debug4("lock acquired");
|
|
@@ -297,8 +297,8 @@ async function releaseLock(lockHandle) {
|
|
|
297
297
|
debug4("error releasing lock: %O", error);
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
|
-
async function withLock(operation) {
|
|
301
|
-
const handle = await acquireLock();
|
|
300
|
+
async function withLock(operation, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
301
|
+
const handle = await acquireLock(timeoutMs);
|
|
302
302
|
if (handle === null) {
|
|
303
303
|
throw new Error("Failed to acquire token refresh lock");
|
|
304
304
|
}
|
|
@@ -1159,17 +1159,90 @@ function applyMarks(text, marks) {
|
|
|
1159
1159
|
return text;
|
|
1160
1160
|
}
|
|
1161
1161
|
|
|
1162
|
+
// src/lib/filters.ts
|
|
1163
|
+
var debug16 = createGranolaDebug("lib:filters");
|
|
1164
|
+
function isSameDay(d1, d2) {
|
|
1165
|
+
return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
|
|
1166
|
+
}
|
|
1167
|
+
function startOfDay(date) {
|
|
1168
|
+
const result = new Date(date);
|
|
1169
|
+
result.setHours(0, 0, 0, 0);
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
function endOfDay(date) {
|
|
1173
|
+
const result = new Date(date);
|
|
1174
|
+
result.setHours(23, 59, 59, 999);
|
|
1175
|
+
return result;
|
|
1176
|
+
}
|
|
1177
|
+
function matchesSearch(meeting, query) {
|
|
1178
|
+
const normalizedQuery = query.toLowerCase();
|
|
1179
|
+
const normalizedTitle = meeting.title.toLowerCase();
|
|
1180
|
+
return normalizedTitle.includes(normalizedQuery);
|
|
1181
|
+
}
|
|
1182
|
+
function matchesAttendee(meeting, query) {
|
|
1183
|
+
const normalizedQuery = query.toLowerCase();
|
|
1184
|
+
const peopleAttendees = meeting.people?.attendees ?? [];
|
|
1185
|
+
const topLevelAttendees = meeting.attendees ?? [];
|
|
1186
|
+
const allAttendees = [...peopleAttendees, ...topLevelAttendees];
|
|
1187
|
+
return allAttendees.some((attendee) => {
|
|
1188
|
+
const name = attendee.name?.toLowerCase() ?? "";
|
|
1189
|
+
const email = attendee.email?.toLowerCase() ?? "";
|
|
1190
|
+
return name.includes(normalizedQuery) || email.includes(normalizedQuery);
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
function matchesDate(meeting, date) {
|
|
1194
|
+
const meetingDate = new Date(meeting.created_at);
|
|
1195
|
+
return isSameDay(meetingDate, date);
|
|
1196
|
+
}
|
|
1197
|
+
function matchesDateRange(meeting, since, until) {
|
|
1198
|
+
const meetingDate = new Date(meeting.created_at);
|
|
1199
|
+
if (since && meetingDate < startOfDay(since)) {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
if (until && meetingDate > endOfDay(until)) {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
function hasActiveFilters(options) {
|
|
1208
|
+
return !!(options.search || options.attendee || options.date || options.since || options.until);
|
|
1209
|
+
}
|
|
1210
|
+
function applyFilters(meetings, options) {
|
|
1211
|
+
if (!hasActiveFilters(options)) {
|
|
1212
|
+
return meetings;
|
|
1213
|
+
}
|
|
1214
|
+
debug16("applying filters: %O", options);
|
|
1215
|
+
const startCount = meetings.length;
|
|
1216
|
+
const filtered = meetings.filter((meeting) => {
|
|
1217
|
+
if (options.search && !matchesSearch(meeting, options.search)) {
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
if (options.attendee && !matchesAttendee(meeting, options.attendee)) {
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
if (options.date && !matchesDate(meeting, options.date)) {
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
if (!matchesDateRange(meeting, options.since, options.until)) {
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
return true;
|
|
1230
|
+
});
|
|
1231
|
+
debug16("filtered %d -> %d meetings", startCount, filtered.length);
|
|
1232
|
+
return filtered;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1162
1235
|
// src/services/meetings.ts
|
|
1163
|
-
var
|
|
1236
|
+
var debug17 = createGranolaDebug("service:meetings");
|
|
1164
1237
|
async function getFolderDocumentIds(client2, folderId) {
|
|
1165
|
-
|
|
1238
|
+
debug17("fetching folder %s via getDocumentList", folderId);
|
|
1166
1239
|
const folder = await client2.getDocumentList(folderId);
|
|
1167
1240
|
if (!folder) {
|
|
1168
|
-
|
|
1241
|
+
debug17("folder %s not found", folderId);
|
|
1169
1242
|
return [];
|
|
1170
1243
|
}
|
|
1171
1244
|
const ids = folder.document_ids || folder.documents?.map((doc) => doc.id) || [];
|
|
1172
|
-
|
|
1245
|
+
debug17("folder %s returned %d document ids", folderId, ids.length);
|
|
1173
1246
|
return ids;
|
|
1174
1247
|
}
|
|
1175
1248
|
var DOCUMENT_BATCH_SIZE = 100;
|
|
@@ -1187,50 +1260,63 @@ async function fetchMeetingsByIds(client2, documentIds) {
|
|
|
1187
1260
|
const docs = res?.documents || res?.docs || [];
|
|
1188
1261
|
meetings.push(...docs);
|
|
1189
1262
|
}
|
|
1190
|
-
|
|
1263
|
+
debug17("fetched %d meetings via getDocumentsBatch", meetings.length);
|
|
1191
1264
|
return meetings;
|
|
1192
1265
|
}
|
|
1193
1266
|
async function loadMeetingMetadata(client2, id) {
|
|
1194
1267
|
try {
|
|
1195
1268
|
const metadata = await client2.getDocumentMetadata(id);
|
|
1196
1269
|
if (!metadata) {
|
|
1197
|
-
|
|
1270
|
+
debug17("getDocumentMetadata returned null for %s", id);
|
|
1198
1271
|
return null;
|
|
1199
1272
|
}
|
|
1200
1273
|
return metadata;
|
|
1201
1274
|
} catch (err) {
|
|
1202
|
-
|
|
1275
|
+
debug17("getDocumentMetadata failed for %s: %O", id, err);
|
|
1203
1276
|
return null;
|
|
1204
1277
|
}
|
|
1205
1278
|
}
|
|
1206
1279
|
async function fetchFolderMeetings(client2, folderId) {
|
|
1207
1280
|
const ids = await getFolderDocumentIds(client2, folderId);
|
|
1208
1281
|
if (ids.length === 0) {
|
|
1209
|
-
|
|
1282
|
+
debug17("folder %s has no documents", folderId);
|
|
1210
1283
|
return [];
|
|
1211
1284
|
}
|
|
1212
1285
|
return fetchMeetingsByIds(client2, ids);
|
|
1213
1286
|
}
|
|
1214
1287
|
async function list2(opts = {}) {
|
|
1215
1288
|
return withTokenRefresh(async () => {
|
|
1216
|
-
|
|
1289
|
+
debug17("list called with opts: %O", opts);
|
|
1217
1290
|
const client2 = await getClient();
|
|
1218
|
-
const { limit = 20, offset = 0, workspace, folder } = opts;
|
|
1291
|
+
const { limit = 20, offset = 0, workspace, folder, ...filterOpts } = opts;
|
|
1219
1292
|
if (folder) {
|
|
1220
|
-
|
|
1293
|
+
debug17("listing meetings for folder: %s", folder);
|
|
1221
1294
|
const folderMeetings = await fetchFolderMeetings(client2, folder);
|
|
1222
|
-
|
|
1295
|
+
debug17("fetched %d meetings for folder %s", folderMeetings.length, folder);
|
|
1223
1296
|
let filtered = folderMeetings;
|
|
1224
1297
|
if (workspace) {
|
|
1225
1298
|
filtered = folderMeetings.filter((m) => m.workspace_id === workspace);
|
|
1226
|
-
|
|
1299
|
+
debug17(
|
|
1227
1300
|
"workspace filter applied for folder %s: %d meetings remain",
|
|
1228
1301
|
folder,
|
|
1229
1302
|
filtered.length
|
|
1230
1303
|
);
|
|
1231
1304
|
}
|
|
1305
|
+
filtered = applyFilters(filtered, filterOpts);
|
|
1232
1306
|
const paginated = filtered.slice(offset, offset + limit);
|
|
1233
|
-
|
|
1307
|
+
debug17("returning %d meetings from folder %s after pagination", paginated.length, folder);
|
|
1308
|
+
return paginated;
|
|
1309
|
+
}
|
|
1310
|
+
if (hasActiveFilters(filterOpts)) {
|
|
1311
|
+
debug17("filters active, using cached meetings for filtering");
|
|
1312
|
+
let meetings2 = await getCachedMeetings(client2);
|
|
1313
|
+
if (workspace) {
|
|
1314
|
+
meetings2 = meetings2.filter((m) => m.workspace_id === workspace);
|
|
1315
|
+
debug17("filtered to %d meetings for workspace: %s", meetings2.length, workspace);
|
|
1316
|
+
}
|
|
1317
|
+
meetings2 = applyFilters(meetings2, filterOpts);
|
|
1318
|
+
const paginated = meetings2.slice(offset, offset + limit);
|
|
1319
|
+
debug17("returning %d meetings after filtering and pagination", paginated.length);
|
|
1234
1320
|
return paginated;
|
|
1235
1321
|
}
|
|
1236
1322
|
const res = await client2.getDocuments({
|
|
@@ -1239,10 +1325,10 @@ async function list2(opts = {}) {
|
|
|
1239
1325
|
include_last_viewed_panel: false
|
|
1240
1326
|
});
|
|
1241
1327
|
let meetings = res?.docs || [];
|
|
1242
|
-
|
|
1328
|
+
debug17("fetched %d meetings", meetings.length);
|
|
1243
1329
|
if (workspace) {
|
|
1244
1330
|
meetings = meetings.filter((m) => m.workspace_id === workspace);
|
|
1245
|
-
|
|
1331
|
+
debug17("filtered to %d meetings for workspace: %s", meetings.length, workspace);
|
|
1246
1332
|
}
|
|
1247
1333
|
return meetings;
|
|
1248
1334
|
});
|
|
@@ -1254,10 +1340,10 @@ var CACHE_TTL_MS = 6e4;
|
|
|
1254
1340
|
var meetingsCache = null;
|
|
1255
1341
|
async function getCachedMeetings(client2) {
|
|
1256
1342
|
if (meetingsCache && Date.now() - meetingsCache.timestamp < CACHE_TTL_MS) {
|
|
1257
|
-
|
|
1343
|
+
debug17("using cached meetings (%d items)", meetingsCache.meetings.length);
|
|
1258
1344
|
return meetingsCache.meetings;
|
|
1259
1345
|
}
|
|
1260
|
-
|
|
1346
|
+
debug17("cache miss or expired, fetching meetings");
|
|
1261
1347
|
const meetings = [];
|
|
1262
1348
|
let offset = 0;
|
|
1263
1349
|
for (let page = 0; page < MAX_RESOLVE_PAGES; page += 1) {
|
|
@@ -1274,23 +1360,23 @@ async function getCachedMeetings(client2) {
|
|
|
1274
1360
|
offset += RESOLVE_PAGE_SIZE;
|
|
1275
1361
|
}
|
|
1276
1362
|
meetingsCache = { meetings, timestamp: Date.now() };
|
|
1277
|
-
|
|
1363
|
+
debug17("cached %d meetings", meetings.length);
|
|
1278
1364
|
return meetings;
|
|
1279
1365
|
}
|
|
1280
1366
|
async function resolveId(partialId) {
|
|
1281
1367
|
return withTokenRefresh(async () => {
|
|
1282
|
-
|
|
1368
|
+
debug17("resolving meeting id: %s (length: %d)", partialId, partialId.length);
|
|
1283
1369
|
const client2 = await getClient();
|
|
1284
1370
|
if (partialId.length >= FULL_UUID_LENGTH) {
|
|
1285
|
-
|
|
1371
|
+
debug17("attempting direct lookup for full UUID");
|
|
1286
1372
|
try {
|
|
1287
1373
|
const metadata = await client2.getDocumentMetadata(partialId);
|
|
1288
1374
|
if (metadata) {
|
|
1289
|
-
|
|
1375
|
+
debug17("direct lookup successful for: %s", partialId);
|
|
1290
1376
|
return partialId;
|
|
1291
1377
|
}
|
|
1292
1378
|
} catch {
|
|
1293
|
-
|
|
1379
|
+
debug17("direct lookup failed, falling back to search");
|
|
1294
1380
|
}
|
|
1295
1381
|
}
|
|
1296
1382
|
const meetings = await getCachedMeetings(client2);
|
|
@@ -1299,30 +1385,30 @@ async function resolveId(partialId) {
|
|
|
1299
1385
|
if (meeting.id?.startsWith(partialId)) {
|
|
1300
1386
|
matches.add(meeting.id);
|
|
1301
1387
|
if (matches.size > 1) {
|
|
1302
|
-
|
|
1388
|
+
debug17("ambiguous id: %s matches >1 meetings", partialId);
|
|
1303
1389
|
throw new Error(`Ambiguous ID: ${partialId} matches ${matches.size} meetings`);
|
|
1304
1390
|
}
|
|
1305
1391
|
}
|
|
1306
1392
|
}
|
|
1307
1393
|
if (matches.size === 0) {
|
|
1308
|
-
|
|
1394
|
+
debug17("no meeting found for id: %s", partialId);
|
|
1309
1395
|
return null;
|
|
1310
1396
|
}
|
|
1311
1397
|
const match = matches.values().next().value;
|
|
1312
|
-
|
|
1398
|
+
debug17("resolved meeting: %s -> %s", partialId, match);
|
|
1313
1399
|
return match;
|
|
1314
1400
|
});
|
|
1315
1401
|
}
|
|
1316
1402
|
async function get2(id) {
|
|
1317
1403
|
return withTokenRefresh(async () => {
|
|
1318
|
-
|
|
1404
|
+
debug17("getting meeting: %s", id);
|
|
1319
1405
|
const client2 = await getClient();
|
|
1320
1406
|
const metadata = await loadMeetingMetadata(client2, id);
|
|
1321
1407
|
if (!metadata) {
|
|
1322
|
-
|
|
1408
|
+
debug17("meeting %s: not found", id);
|
|
1323
1409
|
return null;
|
|
1324
1410
|
}
|
|
1325
|
-
|
|
1411
|
+
debug17("meeting %s: found", id);
|
|
1326
1412
|
return { id, ...metadata };
|
|
1327
1413
|
});
|
|
1328
1414
|
}
|
|
@@ -1330,36 +1416,36 @@ async function findMeetingViaDocuments(client2, id, { includeLastViewedPanel })
|
|
|
1330
1416
|
let offset = 0;
|
|
1331
1417
|
for (let page = 0; page < MAX_NOTES_PAGES; page += 1) {
|
|
1332
1418
|
try {
|
|
1333
|
-
|
|
1419
|
+
debug17("findMeetingViaDocuments fetching page %d (offset: %d)", page, offset);
|
|
1334
1420
|
const res = await client2.getDocuments({
|
|
1335
1421
|
limit: NOTES_PAGE_SIZE,
|
|
1336
1422
|
offset,
|
|
1337
1423
|
include_last_viewed_panel: includeLastViewedPanel
|
|
1338
1424
|
});
|
|
1339
1425
|
const meetings = res?.docs || [];
|
|
1340
|
-
|
|
1426
|
+
debug17("findMeetingViaDocuments got %d meetings on page %d", meetings.length, page);
|
|
1341
1427
|
if (meetings.length === 0) break;
|
|
1342
1428
|
const meeting = meetings.find((m) => m.id === id);
|
|
1343
1429
|
if (meeting) {
|
|
1344
|
-
|
|
1430
|
+
debug17("findMeetingViaDocuments located meeting %s on page %d", id, page);
|
|
1345
1431
|
return meeting;
|
|
1346
1432
|
}
|
|
1347
1433
|
offset += NOTES_PAGE_SIZE;
|
|
1348
1434
|
} catch (err) {
|
|
1349
|
-
|
|
1435
|
+
debug17("findMeetingViaDocuments error: %O", err);
|
|
1350
1436
|
return null;
|
|
1351
1437
|
}
|
|
1352
1438
|
}
|
|
1353
|
-
|
|
1439
|
+
debug17("findMeetingViaDocuments did not locate meeting %s", id);
|
|
1354
1440
|
return null;
|
|
1355
1441
|
}
|
|
1356
1442
|
async function getNotes(id) {
|
|
1357
1443
|
return withTokenRefresh(async () => {
|
|
1358
|
-
|
|
1444
|
+
debug17("getNotes called with id: %s", id);
|
|
1359
1445
|
const client2 = await getClient();
|
|
1360
1446
|
const metadata = await loadMeetingMetadata(client2, id);
|
|
1361
1447
|
if (metadata && "notes" in metadata) {
|
|
1362
|
-
|
|
1448
|
+
debug17("getNotes resolved via metadata response");
|
|
1363
1449
|
return metadata.notes || null;
|
|
1364
1450
|
}
|
|
1365
1451
|
const meeting = await findMeetingViaDocuments(client2, id, {
|
|
@@ -1373,11 +1459,11 @@ async function getNotes(id) {
|
|
|
1373
1459
|
}
|
|
1374
1460
|
async function getEnhancedNotes(id) {
|
|
1375
1461
|
return withTokenRefresh(async () => {
|
|
1376
|
-
|
|
1462
|
+
debug17("getEnhancedNotes called with id: %s", id);
|
|
1377
1463
|
const client2 = await getClient();
|
|
1378
1464
|
const metadata = await loadMeetingMetadata(client2, id);
|
|
1379
1465
|
if (metadata && "last_viewed_panel" in metadata) {
|
|
1380
|
-
|
|
1466
|
+
debug17("getEnhancedNotes resolved via metadata response");
|
|
1381
1467
|
return metadata.last_viewed_panel?.content || null;
|
|
1382
1468
|
}
|
|
1383
1469
|
const meeting = await findMeetingViaDocuments(client2, id, {
|
|
@@ -1391,24 +1477,24 @@ async function getEnhancedNotes(id) {
|
|
|
1391
1477
|
}
|
|
1392
1478
|
async function getTranscript(id) {
|
|
1393
1479
|
return withTokenRefresh(async () => {
|
|
1394
|
-
|
|
1480
|
+
debug17("getTranscript called with id: %s", id);
|
|
1395
1481
|
const client2 = await getClient();
|
|
1396
1482
|
try {
|
|
1397
1483
|
const transcript = await client2.getDocumentTranscript(id);
|
|
1398
|
-
|
|
1484
|
+
debug17("getTranscript got %d utterances", transcript.length);
|
|
1399
1485
|
return transcript;
|
|
1400
1486
|
} catch (err) {
|
|
1401
|
-
|
|
1487
|
+
debug17("getTranscript error: %O", err);
|
|
1402
1488
|
return [];
|
|
1403
1489
|
}
|
|
1404
1490
|
});
|
|
1405
1491
|
}
|
|
1406
1492
|
|
|
1407
1493
|
// src/commands/meeting/enhanced.ts
|
|
1408
|
-
var
|
|
1494
|
+
var debug18 = createGranolaDebug("cmd:meeting:enhanced");
|
|
1409
1495
|
function createEnhancedCommand() {
|
|
1410
1496
|
return new Command10("enhanced").description("View AI-enhanced meeting notes").argument("<id>", "Meeting ID").option("-o, --output <format>", "Output format (markdown, json, yaml, toon)", "markdown").action(async (id, opts, cmd) => {
|
|
1411
|
-
|
|
1497
|
+
debug18("enhanced command invoked with id: %s", id);
|
|
1412
1498
|
const global = cmd.optsWithGlobals();
|
|
1413
1499
|
let fullId;
|
|
1414
1500
|
try {
|
|
@@ -1426,7 +1512,7 @@ function createEnhancedCommand() {
|
|
|
1426
1512
|
try {
|
|
1427
1513
|
notes = await getEnhancedNotes(fullId);
|
|
1428
1514
|
} catch (error) {
|
|
1429
|
-
|
|
1515
|
+
debug18("failed to load enhanced notes: %O", error);
|
|
1430
1516
|
console.error(chalk10.red("Error:"), "Failed to fetch enhanced notes.");
|
|
1431
1517
|
if (error instanceof Error) {
|
|
1432
1518
|
console.error(chalk10.dim(error.message));
|
|
@@ -1470,10 +1556,10 @@ function toToon(data) {
|
|
|
1470
1556
|
}
|
|
1471
1557
|
|
|
1472
1558
|
// src/commands/meeting/export.ts
|
|
1473
|
-
var
|
|
1559
|
+
var debug19 = createGranolaDebug("cmd:meeting:export");
|
|
1474
1560
|
function createExportCommand() {
|
|
1475
1561
|
return new Command11("export").description("Export meeting data").argument("<id>", "Meeting ID").option("-f, --format <format>", "Output format (json, toon)", "json").action(async (id, options) => {
|
|
1476
|
-
|
|
1562
|
+
debug19("export command invoked with id: %s, format: %s", id, options.format);
|
|
1477
1563
|
const format = options.format;
|
|
1478
1564
|
if (format !== "json" && format !== "toon") {
|
|
1479
1565
|
console.error(chalk11.red(`Invalid format: ${options.format}. Use 'json' or 'toon'.`));
|
|
@@ -1523,10 +1609,132 @@ var exportCommand = createExportCommand();
|
|
|
1523
1609
|
// src/commands/meeting/list.ts
|
|
1524
1610
|
import chalk12 from "chalk";
|
|
1525
1611
|
import { Command as Command12 } from "commander";
|
|
1526
|
-
|
|
1612
|
+
|
|
1613
|
+
// src/lib/date-parser.ts
|
|
1614
|
+
var debug20 = createGranolaDebug("lib:date-parser");
|
|
1615
|
+
var MONTH_NAMES = {
|
|
1616
|
+
jan: 0,
|
|
1617
|
+
january: 0,
|
|
1618
|
+
feb: 1,
|
|
1619
|
+
february: 1,
|
|
1620
|
+
mar: 2,
|
|
1621
|
+
march: 2,
|
|
1622
|
+
apr: 3,
|
|
1623
|
+
april: 3,
|
|
1624
|
+
may: 4,
|
|
1625
|
+
jun: 5,
|
|
1626
|
+
june: 5,
|
|
1627
|
+
jul: 6,
|
|
1628
|
+
july: 6,
|
|
1629
|
+
aug: 7,
|
|
1630
|
+
august: 7,
|
|
1631
|
+
sep: 8,
|
|
1632
|
+
september: 8,
|
|
1633
|
+
oct: 9,
|
|
1634
|
+
october: 9,
|
|
1635
|
+
nov: 10,
|
|
1636
|
+
november: 10,
|
|
1637
|
+
dec: 11,
|
|
1638
|
+
december: 11
|
|
1639
|
+
};
|
|
1640
|
+
function addDays(date, days) {
|
|
1641
|
+
const result = new Date(date);
|
|
1642
|
+
result.setDate(result.getDate() + days);
|
|
1643
|
+
return result;
|
|
1644
|
+
}
|
|
1645
|
+
function addMonths(date, months) {
|
|
1646
|
+
const result = new Date(date);
|
|
1647
|
+
result.setMonth(result.getMonth() + months);
|
|
1648
|
+
return result;
|
|
1649
|
+
}
|
|
1650
|
+
function startOfDay2(date) {
|
|
1651
|
+
const result = new Date(date);
|
|
1652
|
+
result.setHours(0, 0, 0, 0);
|
|
1653
|
+
return result;
|
|
1654
|
+
}
|
|
1655
|
+
function parseDate(input) {
|
|
1656
|
+
const normalized = input.trim().toLowerCase();
|
|
1657
|
+
debug20("parsing date: %s", normalized);
|
|
1658
|
+
if (normalized === "today") {
|
|
1659
|
+
return startOfDay2(/* @__PURE__ */ new Date());
|
|
1660
|
+
}
|
|
1661
|
+
if (normalized === "yesterday") {
|
|
1662
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), -1));
|
|
1663
|
+
}
|
|
1664
|
+
if (normalized === "tomorrow") {
|
|
1665
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), 1));
|
|
1666
|
+
}
|
|
1667
|
+
if (normalized === "last week") {
|
|
1668
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), -7));
|
|
1669
|
+
}
|
|
1670
|
+
if (normalized === "last month") {
|
|
1671
|
+
return startOfDay2(addMonths(/* @__PURE__ */ new Date(), -1));
|
|
1672
|
+
}
|
|
1673
|
+
const daysAgoMatch = normalized.match(/^(\d+)\s+days?\s+ago$/);
|
|
1674
|
+
if (daysAgoMatch) {
|
|
1675
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), -Number.parseInt(daysAgoMatch[1], 10)));
|
|
1676
|
+
}
|
|
1677
|
+
const weeksAgoMatch = normalized.match(/^(\d+)\s+weeks?\s+ago$/);
|
|
1678
|
+
if (weeksAgoMatch) {
|
|
1679
|
+
return startOfDay2(addDays(/* @__PURE__ */ new Date(), -Number.parseInt(weeksAgoMatch[1], 10) * 7));
|
|
1680
|
+
}
|
|
1681
|
+
const monthsAgoMatch = normalized.match(/^(\d+)\s+months?\s+ago$/);
|
|
1682
|
+
if (monthsAgoMatch) {
|
|
1683
|
+
return startOfDay2(addMonths(/* @__PURE__ */ new Date(), -Number.parseInt(monthsAgoMatch[1], 10)));
|
|
1684
|
+
}
|
|
1685
|
+
const isoMatch = input.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})$/);
|
|
1686
|
+
if (isoMatch) {
|
|
1687
|
+
const year = Number.parseInt(isoMatch[1], 10);
|
|
1688
|
+
const month = Number.parseInt(isoMatch[2], 10) - 1;
|
|
1689
|
+
const day = Number.parseInt(isoMatch[3], 10);
|
|
1690
|
+
const date = new Date(year, month, day);
|
|
1691
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1692
|
+
return startOfDay2(date);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const monthDayMatch = normalized.match(/^([a-z]+)\s+(\d{1,2})(?:\s+(\d{4}))?$/);
|
|
1696
|
+
if (monthDayMatch) {
|
|
1697
|
+
const monthNum = MONTH_NAMES[monthDayMatch[1]];
|
|
1698
|
+
if (monthNum !== void 0) {
|
|
1699
|
+
const day = Number.parseInt(monthDayMatch[2], 10);
|
|
1700
|
+
const year = monthDayMatch[3] ? Number.parseInt(monthDayMatch[3], 10) : (/* @__PURE__ */ new Date()).getFullYear();
|
|
1701
|
+
const date = new Date(year, monthNum, day);
|
|
1702
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1703
|
+
return startOfDay2(date);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
const dayMonthMatch = normalized.match(/^(\d{1,2})\s+([a-z]+)(?:\s+(\d{4}))?$/);
|
|
1708
|
+
if (dayMonthMatch) {
|
|
1709
|
+
const monthNum = MONTH_NAMES[dayMonthMatch[2]];
|
|
1710
|
+
if (monthNum !== void 0) {
|
|
1711
|
+
const day = Number.parseInt(dayMonthMatch[1], 10);
|
|
1712
|
+
const year = dayMonthMatch[3] ? Number.parseInt(dayMonthMatch[3], 10) : (/* @__PURE__ */ new Date()).getFullYear();
|
|
1713
|
+
const date = new Date(year, monthNum, day);
|
|
1714
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1715
|
+
return startOfDay2(date);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
debug20("failed to parse date: %s", input);
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
function validateDateOption(value, optionName) {
|
|
1723
|
+
const parsed = parseDate(value);
|
|
1724
|
+
if (!parsed) {
|
|
1725
|
+
throw new Error(
|
|
1726
|
+
`Invalid date for ${optionName}: "${value}". Try formats like: today, yesterday, last week, 2024-01-15, "Dec 20"`
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
debug20("validated %s: %s -> %s", optionName, value, parsed.toISOString());
|
|
1730
|
+
return parsed;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/commands/meeting/list.ts
|
|
1734
|
+
var debug21 = createGranolaDebug("cmd:meeting:list");
|
|
1527
1735
|
function createListCommand2() {
|
|
1528
|
-
return new Command12("list").description("List meetings").option("-l, --limit <n>", "Number of meetings", "20").option("-w, --workspace <id>", "Filter by workspace").option("-f, --folder <id>", "Filter by folder").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
|
|
1529
|
-
|
|
1736
|
+
return new Command12("list").description("List meetings").option("-l, --limit <n>", "Number of meetings", "20").option("-w, --workspace <id>", "Filter by workspace").option("-f, --folder <id>", "Filter by folder").option("-s, --search <query>", "Search in meeting titles").option("-a, --attendee <name>", "Filter by attendee name or email").option("-d, --date <date>", "Filter meetings on a specific date").option("--since <date>", "Filter meetings from date (inclusive)").option("--until <date>", "Filter meetings up to date (inclusive)").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
|
|
1737
|
+
debug21("list command invoked with opts: %O", opts);
|
|
1530
1738
|
const limit = Number.parseInt(opts.limit, 10);
|
|
1531
1739
|
if (!Number.isFinite(limit) || limit < 1) {
|
|
1532
1740
|
console.error(chalk12.red("Invalid --limit value. Please provide a positive number."));
|
|
@@ -1534,14 +1742,40 @@ function createListCommand2() {
|
|
|
1534
1742
|
}
|
|
1535
1743
|
const configuredWorkspace = getConfigValue("default_workspace");
|
|
1536
1744
|
const workspace = opts.workspace ?? configuredWorkspace;
|
|
1745
|
+
let date;
|
|
1746
|
+
let since;
|
|
1747
|
+
let until;
|
|
1748
|
+
try {
|
|
1749
|
+
if (opts.date) {
|
|
1750
|
+
date = validateDateOption(opts.date, "--date");
|
|
1751
|
+
}
|
|
1752
|
+
if (opts.since) {
|
|
1753
|
+
since = validateDateOption(opts.since, "--since");
|
|
1754
|
+
}
|
|
1755
|
+
if (opts.until) {
|
|
1756
|
+
until = validateDateOption(opts.until, "--until");
|
|
1757
|
+
}
|
|
1758
|
+
} catch (err) {
|
|
1759
|
+
console.error(chalk12.red(err.message));
|
|
1760
|
+
process.exit(1);
|
|
1761
|
+
}
|
|
1762
|
+
if (since && until && since > until) {
|
|
1763
|
+
console.error(chalk12.red("--since date must be before --until date"));
|
|
1764
|
+
process.exit(1);
|
|
1765
|
+
}
|
|
1537
1766
|
const data = await list2({
|
|
1538
1767
|
limit,
|
|
1539
1768
|
workspace,
|
|
1540
|
-
folder: opts.folder
|
|
1769
|
+
folder: opts.folder,
|
|
1770
|
+
search: opts.search,
|
|
1771
|
+
attendee: opts.attendee,
|
|
1772
|
+
date,
|
|
1773
|
+
since,
|
|
1774
|
+
until
|
|
1541
1775
|
});
|
|
1542
|
-
|
|
1776
|
+
debug21("fetched %d meetings", data.length);
|
|
1543
1777
|
const format = opts.output || null;
|
|
1544
|
-
|
|
1778
|
+
debug21("output format: %s", format || "table");
|
|
1545
1779
|
if (format) {
|
|
1546
1780
|
if (!["json", "yaml", "toon"].includes(format)) {
|
|
1547
1781
|
console.error(chalk12.red(`Invalid format: ${format}. Use 'json', 'yaml', or 'toon'.`));
|
|
@@ -1569,10 +1803,10 @@ var listCommand2 = createListCommand2();
|
|
|
1569
1803
|
// src/commands/meeting/notes.ts
|
|
1570
1804
|
import chalk13 from "chalk";
|
|
1571
1805
|
import { Command as Command13 } from "commander";
|
|
1572
|
-
var
|
|
1806
|
+
var debug22 = createGranolaDebug("cmd:meeting:notes");
|
|
1573
1807
|
function createNotesCommand() {
|
|
1574
1808
|
return new Command13("notes").description("View meeting notes").argument("<id>", "Meeting ID").option("-o, --output <format>", "Output format (markdown, json, yaml, toon)", "markdown").action(async (id, opts, cmd) => {
|
|
1575
|
-
|
|
1809
|
+
debug22("notes command invoked with id: %s", id);
|
|
1576
1810
|
const global = cmd.optsWithGlobals();
|
|
1577
1811
|
let fullId;
|
|
1578
1812
|
try {
|
|
@@ -1590,7 +1824,7 @@ function createNotesCommand() {
|
|
|
1590
1824
|
try {
|
|
1591
1825
|
notes = await getNotes(fullId);
|
|
1592
1826
|
} catch (error) {
|
|
1593
|
-
|
|
1827
|
+
debug22("failed to load notes: %O", error);
|
|
1594
1828
|
console.error(chalk13.red("Error:"), "Failed to fetch notes.");
|
|
1595
1829
|
if (error instanceof Error) {
|
|
1596
1830
|
console.error(chalk13.dim(error.message));
|
|
@@ -1628,17 +1862,17 @@ import chalk14 from "chalk";
|
|
|
1628
1862
|
import { Command as Command14 } from "commander";
|
|
1629
1863
|
|
|
1630
1864
|
// src/lib/transcript.ts
|
|
1631
|
-
var
|
|
1865
|
+
var debug23 = createGranolaDebug("lib:transcript");
|
|
1632
1866
|
function formatTranscript(utterances, opts = {}) {
|
|
1633
|
-
|
|
1867
|
+
debug23("formatTranscript: %d utterances, opts=%O", utterances.length, opts);
|
|
1634
1868
|
const { timestamps = false, source = "all" } = opts;
|
|
1635
1869
|
let filtered = utterances;
|
|
1636
1870
|
if (source !== "all") {
|
|
1637
1871
|
filtered = utterances.filter((u) => u.source === source);
|
|
1638
|
-
|
|
1872
|
+
debug23("filtered to %d utterances (source=%s)", filtered.length, source);
|
|
1639
1873
|
}
|
|
1640
1874
|
if (filtered.length === 0) {
|
|
1641
|
-
|
|
1875
|
+
debug23("no transcript available");
|
|
1642
1876
|
return "No transcript available.";
|
|
1643
1877
|
}
|
|
1644
1878
|
const lines = [];
|
|
@@ -1665,11 +1899,11 @@ function formatTimestamp(iso) {
|
|
|
1665
1899
|
}
|
|
1666
1900
|
|
|
1667
1901
|
// src/commands/meeting/transcript.ts
|
|
1668
|
-
var
|
|
1902
|
+
var debug24 = createGranolaDebug("cmd:meeting:transcript");
|
|
1669
1903
|
var SOURCE_OPTIONS = /* @__PURE__ */ new Set(["microphone", "system", "all"]);
|
|
1670
1904
|
function createTranscriptCommand() {
|
|
1671
1905
|
return new Command14("transcript").description("View meeting transcript").argument("<id>", "Meeting ID").option("-t, --timestamps", "Include timestamps").option("-s, --source <type>", "Filter: microphone, system, all", "all").option("-o, --output <format>", "Output format (text, json, yaml, toon)", "text").action(async (id, opts, cmd) => {
|
|
1672
|
-
|
|
1906
|
+
debug24("transcript command invoked with id: %s, opts: %O", id, opts);
|
|
1673
1907
|
const global = cmd.optsWithGlobals();
|
|
1674
1908
|
let fullId;
|
|
1675
1909
|
try {
|
|
@@ -1724,10 +1958,10 @@ var transcriptCommand = createTranscriptCommand();
|
|
|
1724
1958
|
import chalk15 from "chalk";
|
|
1725
1959
|
import { Command as Command15 } from "commander";
|
|
1726
1960
|
import open2 from "open";
|
|
1727
|
-
var
|
|
1961
|
+
var debug25 = createGranolaDebug("cmd:meeting:view");
|
|
1728
1962
|
function createViewCommand2() {
|
|
1729
1963
|
return new Command15("view").description("View meeting details").argument("<id>", "Meeting ID").option("--web", "Open in browser").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (id, opts) => {
|
|
1730
|
-
|
|
1964
|
+
debug25("view command invoked with id: %s, opts: %O", id, opts);
|
|
1731
1965
|
let fullId;
|
|
1732
1966
|
try {
|
|
1733
1967
|
const resolved = await resolveId(id);
|
|
@@ -1741,7 +1975,7 @@ function createViewCommand2() {
|
|
|
1741
1975
|
process.exit(1);
|
|
1742
1976
|
}
|
|
1743
1977
|
if (opts.web) {
|
|
1744
|
-
await open2(`https://
|
|
1978
|
+
await open2(`https://notes.granola.ai/d/${fullId}`);
|
|
1745
1979
|
return;
|
|
1746
1980
|
}
|
|
1747
1981
|
const meeting = await get2(fullId);
|
|
@@ -1792,14 +2026,14 @@ import chalk16 from "chalk";
|
|
|
1792
2026
|
import { Command as Command17 } from "commander";
|
|
1793
2027
|
|
|
1794
2028
|
// src/services/workspaces.ts
|
|
1795
|
-
var
|
|
2029
|
+
var debug26 = createGranolaDebug("service:workspaces");
|
|
1796
2030
|
async function list3() {
|
|
1797
2031
|
return withTokenRefresh(async () => {
|
|
1798
|
-
|
|
2032
|
+
debug26("fetching workspaces");
|
|
1799
2033
|
const client2 = await getClient();
|
|
1800
2034
|
const res = await client2.getWorkspaces();
|
|
1801
2035
|
const workspacesArray = res?.workspaces || [];
|
|
1802
|
-
|
|
2036
|
+
debug26("found %d workspaces", workspacesArray.length);
|
|
1803
2037
|
return workspacesArray.map((item) => {
|
|
1804
2038
|
const ws = item.workspace;
|
|
1805
2039
|
return {
|
|
@@ -1812,35 +2046,35 @@ async function list3() {
|
|
|
1812
2046
|
});
|
|
1813
2047
|
}
|
|
1814
2048
|
async function resolveId2(partialId) {
|
|
1815
|
-
|
|
2049
|
+
debug26("resolving workspace id: %s", partialId);
|
|
1816
2050
|
const workspaces = await list3();
|
|
1817
2051
|
const matches = workspaces.filter((w) => w.id.startsWith(partialId));
|
|
1818
2052
|
if (matches.length === 0) {
|
|
1819
|
-
|
|
2053
|
+
debug26("no workspace found for id: %s", partialId);
|
|
1820
2054
|
return null;
|
|
1821
2055
|
}
|
|
1822
2056
|
if (matches.length > 1) {
|
|
1823
|
-
|
|
2057
|
+
debug26("ambiguous id: %s matches %d workspaces", partialId, matches.length);
|
|
1824
2058
|
throw new Error(`Ambiguous ID: ${partialId} matches ${matches.length} workspaces`);
|
|
1825
2059
|
}
|
|
1826
|
-
|
|
2060
|
+
debug26("resolved workspace: %s -> %s", partialId, matches[0].id);
|
|
1827
2061
|
return matches[0].id;
|
|
1828
2062
|
}
|
|
1829
2063
|
async function get3(id) {
|
|
1830
|
-
|
|
2064
|
+
debug26("getting workspace: %s", id);
|
|
1831
2065
|
const workspaces = await list3();
|
|
1832
2066
|
const workspace = workspaces.find((w) => w.id === id) || null;
|
|
1833
|
-
|
|
2067
|
+
debug26("workspace %s: %s", id, workspace ? "found" : "not found");
|
|
1834
2068
|
return workspace;
|
|
1835
2069
|
}
|
|
1836
2070
|
|
|
1837
2071
|
// src/commands/workspace/list.ts
|
|
1838
|
-
var
|
|
2072
|
+
var debug27 = createGranolaDebug("cmd:workspace:list");
|
|
1839
2073
|
function createListCommand3() {
|
|
1840
2074
|
return new Command17("list").description("List workspaces").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (opts) => {
|
|
1841
|
-
|
|
2075
|
+
debug27("workspace list command invoked");
|
|
1842
2076
|
const data = await list3();
|
|
1843
|
-
|
|
2077
|
+
debug27("fetched %d workspaces", data.length);
|
|
1844
2078
|
const format = opts.output || null;
|
|
1845
2079
|
if (format) {
|
|
1846
2080
|
if (!["json", "yaml", "toon"].includes(format)) {
|
|
@@ -1867,10 +2101,10 @@ var listCommand3 = createListCommand3();
|
|
|
1867
2101
|
// src/commands/workspace/view.ts
|
|
1868
2102
|
import chalk17 from "chalk";
|
|
1869
2103
|
import { Command as Command18 } from "commander";
|
|
1870
|
-
var
|
|
2104
|
+
var debug28 = createGranolaDebug("cmd:workspace:view");
|
|
1871
2105
|
function createViewCommand3() {
|
|
1872
2106
|
return new Command18("view").description("View workspace details").argument("<id>", "Workspace ID").option("-o, --output <format>", "Output format (json, yaml, toon)").action(async (id, opts) => {
|
|
1873
|
-
|
|
2107
|
+
debug28("workspace view command invoked with id: %s", id);
|
|
1874
2108
|
let fullId;
|
|
1875
2109
|
try {
|
|
1876
2110
|
const resolved = await resolveId2(id);
|
|
@@ -1908,12 +2142,36 @@ var viewCommand3 = createViewCommand3();
|
|
|
1908
2142
|
// src/commands/workspace/index.ts
|
|
1909
2143
|
var workspaceCommand = new Command19("workspace").description("Work with workspaces").addCommand(listCommand3).addCommand(viewCommand3);
|
|
1910
2144
|
|
|
2145
|
+
// src/lib/errors.ts
|
|
2146
|
+
import chalk18 from "chalk";
|
|
2147
|
+
function handleGlobalError(error) {
|
|
2148
|
+
if (error instanceof ApiError) {
|
|
2149
|
+
if (error.status === 401) {
|
|
2150
|
+
console.error(chalk18.red("Error:"), "Authentication required.");
|
|
2151
|
+
console.error(`Run ${chalk18.cyan("granola auth login")} to authenticate.`);
|
|
2152
|
+
return 2;
|
|
2153
|
+
}
|
|
2154
|
+
console.error(chalk18.red("Error:"), error.message);
|
|
2155
|
+
return 1;
|
|
2156
|
+
}
|
|
2157
|
+
if (error instanceof Error && error.message.includes("fetch failed")) {
|
|
2158
|
+
console.error(chalk18.red("Error:"), "Network error. Check your connection.");
|
|
2159
|
+
return 1;
|
|
2160
|
+
}
|
|
2161
|
+
if (error instanceof Error) {
|
|
2162
|
+
console.error(chalk18.red("Error:"), error.message || "An unexpected error occurred.");
|
|
2163
|
+
} else {
|
|
2164
|
+
console.error(chalk18.red("Error:"), "An unexpected error occurred.");
|
|
2165
|
+
}
|
|
2166
|
+
return 1;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
1911
2169
|
// src/main.ts
|
|
1912
|
-
var
|
|
2170
|
+
var debug29 = createGranolaDebug("cli");
|
|
1913
2171
|
var debugAlias = createGranolaDebug("cli:alias");
|
|
1914
2172
|
var packageJson = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8"));
|
|
1915
|
-
|
|
1916
|
-
|
|
2173
|
+
debug29("granola-cli v%s starting", packageJson.version);
|
|
2174
|
+
debug29("arguments: %O", process.argv.slice(2));
|
|
1917
2175
|
var program = new Command20();
|
|
1918
2176
|
program.name("granola").description("CLI for Granola meeting notes").version(packageJson.version).option("--no-pager", "Disable pager");
|
|
1919
2177
|
program.addCommand(authCommand);
|
|
@@ -1942,6 +2200,9 @@ function expandAlias(args) {
|
|
|
1942
2200
|
return args;
|
|
1943
2201
|
}
|
|
1944
2202
|
var expandedArgs = expandAlias(process.argv);
|
|
1945
|
-
|
|
1946
|
-
program.parseAsync(expandedArgs)
|
|
2203
|
+
debug29("parsing with args: %O", expandedArgs.slice(2));
|
|
2204
|
+
program.parseAsync(expandedArgs).catch((error) => {
|
|
2205
|
+
const exitCode = handleGlobalError(error);
|
|
2206
|
+
process.exit(exitCode);
|
|
2207
|
+
});
|
|
1947
2208
|
//# sourceMappingURL=main.js.map
|