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/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 < LOCK_TIMEOUT_MS) {
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 debug16 = createGranolaDebug("service:meetings");
1236
+ var debug17 = createGranolaDebug("service:meetings");
1164
1237
  async function getFolderDocumentIds(client2, folderId) {
1165
- debug16("fetching folder %s via getDocumentList", folderId);
1238
+ debug17("fetching folder %s via getDocumentList", folderId);
1166
1239
  const folder = await client2.getDocumentList(folderId);
1167
1240
  if (!folder) {
1168
- debug16("folder %s not found", folderId);
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
- debug16("folder %s returned %d document ids", folderId, ids.length);
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
- debug16("fetched %d meetings via getDocumentsBatch", meetings.length);
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
- debug16("getDocumentMetadata returned null for %s", id);
1270
+ debug17("getDocumentMetadata returned null for %s", id);
1198
1271
  return null;
1199
1272
  }
1200
1273
  return metadata;
1201
1274
  } catch (err) {
1202
- debug16("getDocumentMetadata failed for %s: %O", id, err);
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
- debug16("folder %s has no documents", folderId);
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
- debug16("list called with opts: %O", opts);
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
- debug16("listing meetings for folder: %s", folder);
1293
+ debug17("listing meetings for folder: %s", folder);
1221
1294
  const folderMeetings = await fetchFolderMeetings(client2, folder);
1222
- debug16("fetched %d meetings for folder %s", folderMeetings.length, folder);
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
- debug16(
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
- debug16("returning %d meetings from folder %s after pagination", paginated.length, folder);
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
- debug16("fetched %d meetings", meetings.length);
1328
+ debug17("fetched %d meetings", meetings.length);
1243
1329
  if (workspace) {
1244
1330
  meetings = meetings.filter((m) => m.workspace_id === workspace);
1245
- debug16("filtered to %d meetings for workspace: %s", meetings.length, workspace);
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
- debug16("using cached meetings (%d items)", meetingsCache.meetings.length);
1343
+ debug17("using cached meetings (%d items)", meetingsCache.meetings.length);
1258
1344
  return meetingsCache.meetings;
1259
1345
  }
1260
- debug16("cache miss or expired, fetching meetings");
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
- debug16("cached %d meetings", meetings.length);
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
- debug16("resolving meeting id: %s (length: %d)", partialId, partialId.length);
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
- debug16("attempting direct lookup for full UUID");
1371
+ debug17("attempting direct lookup for full UUID");
1286
1372
  try {
1287
1373
  const metadata = await client2.getDocumentMetadata(partialId);
1288
1374
  if (metadata) {
1289
- debug16("direct lookup successful for: %s", partialId);
1375
+ debug17("direct lookup successful for: %s", partialId);
1290
1376
  return partialId;
1291
1377
  }
1292
1378
  } catch {
1293
- debug16("direct lookup failed, falling back to search");
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
- debug16("ambiguous id: %s matches >1 meetings", partialId);
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
- debug16("no meeting found for id: %s", partialId);
1394
+ debug17("no meeting found for id: %s", partialId);
1309
1395
  return null;
1310
1396
  }
1311
1397
  const match = matches.values().next().value;
1312
- debug16("resolved meeting: %s -> %s", partialId, match);
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
- debug16("getting meeting: %s", id);
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
- debug16("meeting %s: not found", id);
1408
+ debug17("meeting %s: not found", id);
1323
1409
  return null;
1324
1410
  }
1325
- debug16("meeting %s: found", id);
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
- debug16("findMeetingViaDocuments fetching page %d (offset: %d)", page, offset);
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
- debug16("findMeetingViaDocuments got %d meetings on page %d", meetings.length, page);
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
- debug16("findMeetingViaDocuments located meeting %s on page %d", id, page);
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
- debug16("findMeetingViaDocuments error: %O", err);
1435
+ debug17("findMeetingViaDocuments error: %O", err);
1350
1436
  return null;
1351
1437
  }
1352
1438
  }
1353
- debug16("findMeetingViaDocuments did not locate meeting %s", id);
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
- debug16("getNotes called with id: %s", id);
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
- debug16("getNotes resolved via metadata response");
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
- debug16("getEnhancedNotes called with id: %s", id);
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
- debug16("getEnhancedNotes resolved via metadata response");
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
- debug16("getTranscript called with id: %s", id);
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
- debug16("getTranscript got %d utterances", transcript.length);
1484
+ debug17("getTranscript got %d utterances", transcript.length);
1399
1485
  return transcript;
1400
1486
  } catch (err) {
1401
- debug16("getTranscript error: %O", err);
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 debug17 = createGranolaDebug("cmd:meeting:enhanced");
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
- debug17("enhanced command invoked with id: %s", id);
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
- debug17("failed to load enhanced notes: %O", error);
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 debug18 = createGranolaDebug("cmd:meeting:export");
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
- debug18("export command invoked with id: %s, format: %s", id, options.format);
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
- var debug19 = createGranolaDebug("cmd:meeting:list");
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
- debug19("list command invoked with opts: %O", opts);
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
- debug19("fetched %d meetings", data.length);
1776
+ debug21("fetched %d meetings", data.length);
1543
1777
  const format = opts.output || null;
1544
- debug19("output format: %s", format || "table");
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 debug20 = createGranolaDebug("cmd:meeting:notes");
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
- debug20("notes command invoked with id: %s", id);
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
- debug20("failed to load notes: %O", error);
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 debug21 = createGranolaDebug("lib:transcript");
1865
+ var debug23 = createGranolaDebug("lib:transcript");
1632
1866
  function formatTranscript(utterances, opts = {}) {
1633
- debug21("formatTranscript: %d utterances, opts=%O", utterances.length, opts);
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
- debug21("filtered to %d utterances (source=%s)", filtered.length, source);
1872
+ debug23("filtered to %d utterances (source=%s)", filtered.length, source);
1639
1873
  }
1640
1874
  if (filtered.length === 0) {
1641
- debug21("no transcript available");
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 debug22 = createGranolaDebug("cmd:meeting:transcript");
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
- debug22("transcript command invoked with id: %s, opts: %O", id, opts);
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 debug23 = createGranolaDebug("cmd:meeting:view");
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
- debug23("view command invoked with id: %s, opts: %O", id, opts);
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://app.granola.ai/meeting/${fullId}`);
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 debug24 = createGranolaDebug("service:workspaces");
2029
+ var debug26 = createGranolaDebug("service:workspaces");
1796
2030
  async function list3() {
1797
2031
  return withTokenRefresh(async () => {
1798
- debug24("fetching workspaces");
2032
+ debug26("fetching workspaces");
1799
2033
  const client2 = await getClient();
1800
2034
  const res = await client2.getWorkspaces();
1801
2035
  const workspacesArray = res?.workspaces || [];
1802
- debug24("found %d workspaces", workspacesArray.length);
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
- debug24("resolving workspace id: %s", partialId);
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
- debug24("no workspace found for id: %s", partialId);
2053
+ debug26("no workspace found for id: %s", partialId);
1820
2054
  return null;
1821
2055
  }
1822
2056
  if (matches.length > 1) {
1823
- debug24("ambiguous id: %s matches %d workspaces", partialId, matches.length);
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
- debug24("resolved workspace: %s -> %s", partialId, matches[0].id);
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
- debug24("getting workspace: %s", id);
2064
+ debug26("getting workspace: %s", id);
1831
2065
  const workspaces = await list3();
1832
2066
  const workspace = workspaces.find((w) => w.id === id) || null;
1833
- debug24("workspace %s: %s", id, workspace ? "found" : "not found");
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 debug25 = createGranolaDebug("cmd:workspace:list");
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
- debug25("workspace list command invoked");
2075
+ debug27("workspace list command invoked");
1842
2076
  const data = await list3();
1843
- debug25("fetched %d workspaces", data.length);
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 debug26 = createGranolaDebug("cmd:workspace:view");
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
- debug26("workspace view command invoked with id: %s", id);
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 debug27 = createGranolaDebug("cli");
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
- debug27("granola-cli v%s starting", packageJson.version);
1916
- debug27("arguments: %O", process.argv.slice(2));
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
- debug27("parsing with args: %O", expandedArgs.slice(2));
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