exponential-cli 1.7.0 → 1.9.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 CHANGED
@@ -70,9 +70,17 @@ exponential actions create -n "Task name" -d "Description" -p <project-id> --pri
70
70
  exponential actions update --id <action-id> --kanban DONE
71
71
  exponential actions update --id <action-id> -n "New name" --priority "2nd Priority" --due 2026-03-15
72
72
 
73
- # Get today's actions
73
+ # Set the do-date (when you plan to work on it). This is what `today` partitions on,
74
+ # and it takes precedence over --due.
75
+ exponential actions update --id <action-id> --scheduled-start 2026-08-05T09:00
76
+ exponential actions update --id <action-id> --scheduled-start null # clear it
77
+
78
+ # What's actually on your plate: overdue + today + inbox
74
79
  exponential actions today
75
80
 
81
+ # Just the actions whose dueDate is today (excludes overdue — see note below)
82
+ exponential actions today --due-only
83
+
76
84
  # Get actions in a date range
77
85
  exponential actions range --start 2024-01-01 --end 2024-01-31
78
86
 
@@ -80,6 +88,48 @@ exponential actions range --start 2024-01-01 --end 2024-01-31
80
88
  exponential actions kanban --project <project-id>
81
89
  ```
82
90
 
91
+ #### Triaging an overdue pile
92
+
93
+ A large overdue count is usually not a large number of missed commitments — it's
94
+ a few bulk writes (a generated project plan, an import) that stamped every row
95
+ with one identical timestamp. `actions overdue` separates the two:
96
+
97
+ ```bash
98
+ exponential actions overdue
99
+ ```
100
+
101
+ ```
102
+ Overdue: 43
103
+
104
+ 21 of these were bulk-created — stamped with one identical timestamp,
105
+ so almost certainly never individually due.
106
+
107
+ 17 actions stamped 2026-07-25T08:29:55.483Z (10d overdue)
108
+ Projects: Entity Money Map, Net Worth Baseline, Tax Reserve System, …
109
+ Amnesty: exponential actions defer --ids cmqcgvtlh…,cmqcgvtnn…,…
110
+
111
+ 19 individually dated — real debt, oldest first.
112
+ 12d Send out the job description for the data engineering role [Hiring]
113
+ ```
114
+
115
+ Then pick a disposition:
116
+
117
+ ```bash
118
+ # Amnesty: clear the dates, back to the project backlog untimed.
119
+ # For work that was never really due on the date it carries.
120
+ exponential actions defer --ids id1,id2,id3
121
+
122
+ # Reschedule: still due, just later.
123
+ exponential actions reschedule --ids id1,id2 --to today
124
+ exponential actions reschedule --ids id1,id2 --to 2026-08-10
125
+ ```
126
+
127
+ > **Note on `actions today`:** as of v1.9.0 this returns the same
128
+ > overdue/today/inbox partition the `/today` page renders, rather than a
129
+ > due-date-only list. The old behaviour silently omitted the overdue pile — it
130
+ > would report 6 actions while 43 sat overdue. Pass `--due-only` for the
131
+ > previous output shape.
132
+
83
133
  ### Projects
84
134
 
85
135
  ```bash
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command18 } from "commander";
4
+ import { Command as Command19 } from "commander";
5
5
 
6
6
  // src/commands/auth.ts
7
7
  import { Command } from "commander";
@@ -329,6 +329,91 @@ ${statusBadge} ${chalk3.bold(action.name)}`);
329
329
  }
330
330
  console.log();
331
331
  }
332
+ function outputTodaysActionsJson(t, filters = {}) {
333
+ console.log(JSON.stringify({
334
+ overdue: { count: t.overdue.count, actions: t.overdue.actions },
335
+ today: { count: t.today.count, actions: t.today.actions },
336
+ inbox: { count: t.inbox.count, actions: t.inbox.actions },
337
+ total: t.overdue.count + t.today.count + t.inbox.count,
338
+ filters
339
+ }, null, 2));
340
+ }
341
+ function printTodayGroup(label, group, color) {
342
+ if (group.count === 0) return;
343
+ const truncated = group.count > group.actions.length ? chalk3.gray(` (showing ${group.actions.length})`) : "";
344
+ console.log(`
345
+ ${chalk3[color].bold(`${label} (${group.count})`)}${truncated}`);
346
+ for (const a of group.actions) {
347
+ const when = a.scheduledStart ?? a.dueDate;
348
+ const stamp = when ? chalk3.gray(formatDate(new Date(when))) : "";
349
+ const project = a.projectName ? chalk3.cyan(` [${a.projectName}]`) : "";
350
+ console.log(` ${a.name}${project} ${stamp}`);
351
+ console.log(chalk3.gray(` ID: ${a.id}`));
352
+ }
353
+ }
354
+ function outputTodaysActionsPretty(t) {
355
+ const total = t.overdue.count + t.today.count + t.inbox.count;
356
+ if (total === 0) {
357
+ console.log(chalk3.gray("Nothing on your plate."));
358
+ return;
359
+ }
360
+ console.log(chalk3.bold("\nOn your plate"));
361
+ console.log(chalk3.gray("\u2500".repeat(50)));
362
+ printTodayGroup("Overdue", t.overdue, "red");
363
+ printTodayGroup("Today", t.today, "green");
364
+ printTodayGroup("Inbox", t.inbox, "gray");
365
+ if (t.overdue.count > 0) {
366
+ console.log(chalk3.gray(`
367
+ Run "exponential actions overdue" to see why the overdue pile is that size.`));
368
+ }
369
+ console.log();
370
+ }
371
+ function outputOverdueTriageJson(t) {
372
+ console.log(JSON.stringify(t, null, 2));
373
+ }
374
+ function outputOverdueTriagePretty(t) {
375
+ if (t.totalOverdue === 0) {
376
+ console.log(chalk3.green("Nothing overdue."));
377
+ return;
378
+ }
379
+ console.log(chalk3.bold(`
380
+ Overdue: ${t.totalOverdue}`));
381
+ console.log(chalk3.gray("\u2500".repeat(50)));
382
+ if (t.cohorts.length > 0) {
383
+ console.log(
384
+ `
385
+ ${chalk3.yellow.bold(`${t.cohortCount} of these were bulk-created`)} ${chalk3.gray("\u2014 stamped with one identical timestamp, so almost certainly never individually due.")}`
386
+ );
387
+ for (const c of t.cohorts) {
388
+ console.log(
389
+ `
390
+ ${chalk3.bold(`${c.count} actions`)} stamped ${chalk3.gray(c.stampedAt.toISOString())} ${chalk3.red(`(${c.daysOverdue}d overdue)`)}`
391
+ );
392
+ if (c.projectNames.length > 0) {
393
+ console.log(` ${chalk3.cyan("Projects:")} ${c.projectNames.join(", ")}`);
394
+ }
395
+ for (const a of c.actions.slice(0, 4)) {
396
+ console.log(chalk3.gray(` \xB7 ${a.name.substring(0, 62)}`));
397
+ }
398
+ if (c.actions.length > 4) {
399
+ console.log(chalk3.gray(` \xB7 \u2026 and ${c.actions.length - 4} more`));
400
+ }
401
+ console.log(
402
+ chalk3.gray(` Amnesty: `) + `exponential actions defer --ids ${c.actionIds.slice(0, 2).join(",")}${c.actionIds.length > 2 ? ",\u2026" : ""}`
403
+ );
404
+ }
405
+ }
406
+ if (t.loose.length > 0) {
407
+ console.log(`
408
+ ${chalk3.bold(`${t.loose.length} individually dated`)} ${chalk3.gray("\u2014 real debt, oldest first.")}`);
409
+ for (const a of t.loose) {
410
+ const project = a.projectName ? chalk3.cyan(` [${a.projectName}]`) : "";
411
+ console.log(` ${chalk3.red(`${String(a.daysOverdue).padStart(3)}d`)} ${a.name.substring(0, 62)}${project}`);
412
+ console.log(chalk3.gray(` ID: ${a.id}`));
413
+ }
414
+ }
415
+ console.log();
416
+ }
332
417
  function outputProjectsJson(projects) {
333
418
  const output = {
334
419
  projects: projects.map(transformProject),
@@ -1312,6 +1397,22 @@ var VALID_PRIORITIES = [
1312
1397
  "Watch",
1313
1398
  "Someday Maybe"
1314
1399
  ];
1400
+ function parseNullableDate(raw, flag) {
1401
+ if (raw === void 0) return void 0;
1402
+ if (raw === "null") return null;
1403
+ const parsed = new Date(raw);
1404
+ if (isNaN(parsed.getTime())) {
1405
+ throw new Error(`Invalid --${flag} value "${raw}". Use YYYY-MM-DD, an ISO datetime, or "null".`);
1406
+ }
1407
+ return parsed;
1408
+ }
1409
+ function parseIds(raw) {
1410
+ const ids = raw.split(",").map((s) => s.trim()).filter(Boolean);
1411
+ if (ids.length === 0) {
1412
+ throw new Error("No action IDs supplied. Pass --ids id1,id2");
1413
+ }
1414
+ return ids;
1415
+ }
1315
1416
  function createActionsCommand() {
1316
1417
  const actions = new Command2("actions").description("Manage actions/tasks");
1317
1418
  actions.command("list").description("List all actions").option("--project <id>", "Filter by project ID").option("--status <status>", "Filter by kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--assignee <id>", "Filter by assignee ID").action(async (options, cmd) => {
@@ -1337,16 +1438,76 @@ function createActionsCommand() {
1337
1438
  handleError(error, useJson);
1338
1439
  }
1339
1440
  });
1340
- actions.command("today").description("Get actions due today").option("--workspace <id>", "Filter by workspace ID").action(async (options, cmd) => {
1441
+ actions.command("today").description("What's on your plate: overdue, today, and inbox").option("--workspace <id>", "Filter by workspace ID").option("--due-only", "Only actions whose dueDate is today (the pre-1.8 shape \u2014 excludes overdue)").action(async (options, cmd) => {
1442
+ const globalOpts = cmd.optsWithGlobals();
1443
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1444
+ try {
1445
+ const client = getClient();
1446
+ if (options.dueOnly) {
1447
+ const actions2 = await client.actions.getToday(options.workspace);
1448
+ if (useJson) {
1449
+ outputActionsJson(actions2, { workspaceId: options.workspace });
1450
+ } else {
1451
+ outputActionsPretty(actions2);
1452
+ }
1453
+ return;
1454
+ }
1455
+ const todays = await client.actions.getTodaysActions(options.workspace);
1456
+ if (useJson) {
1457
+ outputTodaysActionsJson(todays, { workspaceId: options.workspace });
1458
+ } else {
1459
+ outputTodaysActionsPretty(todays);
1460
+ }
1461
+ } catch (error) {
1462
+ handleError(error, useJson);
1463
+ }
1464
+ });
1465
+ actions.command("overdue").description("Why the overdue pile is that size: bulk-created cohorts vs real debt").option("--workspace <id>", "Filter by workspace ID").action(async (options, cmd) => {
1341
1466
  const globalOpts = cmd.optsWithGlobals();
1342
1467
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1343
1468
  try {
1344
1469
  const client = getClient();
1345
- const actions2 = await client.actions.getToday(options.workspace);
1470
+ const triage = await client.actions.getOverdueTriage(options.workspace);
1346
1471
  if (useJson) {
1347
- outputActionsJson(actions2, { workspaceId: options.workspace });
1472
+ outputOverdueTriageJson(triage);
1348
1473
  } else {
1349
- outputActionsPretty(actions2);
1474
+ outputOverdueTriagePretty(triage);
1475
+ }
1476
+ } catch (error) {
1477
+ handleError(error, useJson);
1478
+ }
1479
+ });
1480
+ actions.command("defer").description("Amnesty: clear dates so actions fall back to their project backlog untimed").requiredOption("--ids <ids>", "Comma-separated action IDs").action(async (options, cmd) => {
1481
+ const globalOpts = cmd.optsWithGlobals();
1482
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1483
+ try {
1484
+ const ids = parseIds(options.ids);
1485
+ const client = getClient();
1486
+ const result = await client.actions.bulkDefer(ids);
1487
+ if (useJson) {
1488
+ console.log(JSON.stringify(result, null, 2));
1489
+ } else {
1490
+ console.log(`\u2713 ${result.message}`);
1491
+ }
1492
+ } catch (error) {
1493
+ handleError(error, useJson);
1494
+ }
1495
+ });
1496
+ actions.command("reschedule").description('Move actions to a new do-date (use "actions defer" if they were never really due)').requiredOption("--ids <ids>", "Comma-separated action IDs").requiredOption("--to <date>", 'New do-date (YYYY-MM-DD, or "today")').action(async (options, cmd) => {
1497
+ const globalOpts = cmd.optsWithGlobals();
1498
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1499
+ try {
1500
+ const ids = parseIds(options.ids);
1501
+ const when = options.to === "today" ? /* @__PURE__ */ new Date() : new Date(options.to);
1502
+ if (isNaN(when.getTime())) {
1503
+ throw new Error('Invalid date format. Use YYYY-MM-DD or "today"');
1504
+ }
1505
+ const client = getClient();
1506
+ const result = await client.actions.bulkReschedule(ids, when);
1507
+ if (useJson) {
1508
+ console.log(JSON.stringify(result, null, 2));
1509
+ } else {
1510
+ console.log(`\u2713 Rescheduled ${result.count} action${result.count === 1 ? "" : "s"}`);
1350
1511
  }
1351
1512
  } catch (error) {
1352
1513
  handleError(error, useJson);
@@ -1395,7 +1556,7 @@ function createActionsCommand() {
1395
1556
  handleError(error, useJson);
1396
1557
  }
1397
1558
  });
1398
- actions.command("update").description("Update an existing action").requiredOption("--id <id>", "Action ID to update").option("-n, --name <name>", "New action name").option("-d, --description <text>", "New description").option("-p, --project <id>", "Move to project ID").option("--priority <priority>", "Priority (Quick, Scheduled, 1st Priority, etc.)").option("--status <status>", "Status (ACTIVE, COMPLETED, CANCELLED)").option("--kanban <status>", "Kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--due <date>", 'Due date (YYYY-MM-DD or "null" to clear)').action(async (options, cmd) => {
1559
+ actions.command("update").description("Update an existing action").requiredOption("--id <id>", "Action ID to update").option("-n, --name <name>", "New action name").option("-d, --description <text>", "New description").option("-p, --project <id>", "Move to project ID").option("--priority <priority>", "Priority (Quick, Scheduled, 1st Priority, etc.)").option("--status <status>", "Status (ACTIVE, COMPLETED, CANCELLED)").option("--kanban <status>", "Kanban status (BACKLOG, TODO, IN_PROGRESS, IN_REVIEW, DONE)").option("--due <date>", 'Due date (YYYY-MM-DD or "null" to clear)').option("--scheduled-start <datetime>", 'Do-date: when you plan to work on it (YYYY-MM-DD, ISO datetime, or "null" to clear). This is what /today partitions on.').option("--scheduled-end <datetime>", 'End of the time block (YYYY-MM-DD, ISO datetime, or "null" to clear)').action(async (options, cmd) => {
1399
1560
  const globalOpts = cmd.optsWithGlobals();
1400
1561
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
1401
1562
  try {
@@ -1411,6 +1572,8 @@ function createActionsCommand() {
1411
1572
  throw new Error("Invalid due date format. Use YYYY-MM-DD");
1412
1573
  }
1413
1574
  }
1575
+ const scheduledStart = parseNullableDate(options.scheduledStart, "scheduled-start");
1576
+ const scheduledEnd = parseNullableDate(options.scheduledEnd, "scheduled-end");
1414
1577
  const client = getClient();
1415
1578
  const action = await client.actions.update({
1416
1579
  id: options.id,
@@ -1420,7 +1583,9 @@ function createActionsCommand() {
1420
1583
  priority: options.priority,
1421
1584
  status: options.status,
1422
1585
  kanbanStatus: options.kanban,
1423
- dueDate
1586
+ dueDate,
1587
+ scheduledStart,
1588
+ scheduledEnd
1424
1589
  });
1425
1590
  if (useJson) {
1426
1591
  outputActionJson(action);
@@ -2034,7 +2199,7 @@ function createEpicsCommand() {
2034
2199
  const globalOpts = cmd.optsWithGlobals();
2035
2200
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2036
2201
  try {
2037
- const parseNullableDate = (v) => {
2202
+ const parseNullableDate2 = (v) => {
2038
2203
  if (v === void 0) return void 0;
2039
2204
  if (v === "null") return null;
2040
2205
  const d = new Date(v);
@@ -2053,8 +2218,8 @@ function createEpicsCommand() {
2053
2218
  description,
2054
2219
  status,
2055
2220
  priority,
2056
- startDate: parseNullableDate(options.start),
2057
- targetDate: parseNullableDate(options.target)
2221
+ startDate: parseNullableDate2(options.start),
2222
+ targetDate: parseNullableDate2(options.target)
2058
2223
  });
2059
2224
  if (useJson) outputEpicJson(epic);
2060
2225
  else {
@@ -2500,6 +2665,17 @@ var FEATURE_STATUSES = [
2500
2665
  "DEPRECATED",
2501
2666
  "ARCHIVED"
2502
2667
  ];
2668
+ function summarizeTicket(ticket) {
2669
+ return {
2670
+ id: ticket.id,
2671
+ shortId: ticket.shortId ?? null,
2672
+ number: ticket.number ?? null,
2673
+ title: ticket.title
2674
+ };
2675
+ }
2676
+ function ticketLabel(ticket) {
2677
+ return ticket.shortId ?? (ticket.number != null ? `#${ticket.number}` : ticket.id);
2678
+ }
2503
2679
  function validateFeatureStatus(value) {
2504
2680
  if (!value) return void 0;
2505
2681
  if (!FEATURE_STATUSES.includes(value)) {
@@ -2606,6 +2782,119 @@ function createFeaturesCommand() {
2606
2782
  }
2607
2783
  }
2608
2784
  );
2785
+ features.command("delete <id>").description(
2786
+ "Delete a feature. Refuses if it still has tickets \u2014 deleting a feature does not delete its tickets, it unlinks them (Ticket.featureId is SetNull), silently orphaning them in the product backlog."
2787
+ ).option(
2788
+ "--with-tickets",
2789
+ "Delete the feature's tickets first, then the feature (destructive)"
2790
+ ).option(
2791
+ "--orphan-tickets",
2792
+ "Delete the feature anyway, leaving its tickets in the backlog unlinked"
2793
+ ).action(
2794
+ async (id, options, cmd) => {
2795
+ const globalOpts = cmd.optsWithGlobals();
2796
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
2797
+ try {
2798
+ if (options.withTickets && options.orphanTickets) {
2799
+ throw new Error(
2800
+ "--with-tickets and --orphan-tickets are mutually exclusive"
2801
+ );
2802
+ }
2803
+ const client = getClient();
2804
+ const feature = await client.features.get(id);
2805
+ const tickets = feature._count?.tickets === 0 ? [] : await client.tickets.list({
2806
+ productId: feature.productId,
2807
+ featureId: feature.id
2808
+ });
2809
+ if (tickets.length > 0 && !options.withTickets && !options.orphanTickets) {
2810
+ const reason = `has ${tickets.length} ticket${tickets.length === 1 ? "" : "s"}`;
2811
+ if (useJson) {
2812
+ console.log(
2813
+ JSON.stringify(
2814
+ {
2815
+ deleted: false,
2816
+ id: feature.id,
2817
+ reason,
2818
+ tickets: tickets.map(summarizeTicket)
2819
+ },
2820
+ null,
2821
+ 2
2822
+ )
2823
+ );
2824
+ } else {
2825
+ console.error(
2826
+ `
2827
+ Refusing to delete "${feature.name}": it ${reason}.
2828
+ Deleting the feature would leave them unlinked in the product backlog.
2829
+ --with-tickets delete those tickets first, then the feature
2830
+ --orphan-tickets delete the feature anyway and unlink them`
2831
+ );
2832
+ for (const t of tickets) {
2833
+ console.error(` - ${ticketLabel(t)} ${t.title}`);
2834
+ }
2835
+ }
2836
+ process.exitCode = 1;
2837
+ return;
2838
+ }
2839
+ const ticketsDeleted = [];
2840
+ const ticketsFailed = [];
2841
+ if (options.withTickets) {
2842
+ for (const ticket of tickets) {
2843
+ try {
2844
+ await client.tickets.delete(ticket.id);
2845
+ ticketsDeleted.push(summarizeTicket(ticket));
2846
+ } catch (error) {
2847
+ ticketsFailed.push({
2848
+ ...summarizeTicket(ticket),
2849
+ error: error instanceof Error ? error.message : String(error)
2850
+ });
2851
+ }
2852
+ }
2853
+ }
2854
+ if (ticketsFailed.length > 0) {
2855
+ const reason = `failed to delete ${ticketsFailed.length} of ${tickets.length} tickets`;
2856
+ if (useJson) {
2857
+ console.log(
2858
+ JSON.stringify(
2859
+ { deleted: false, id: feature.id, reason, ticketsDeleted, ticketsFailed },
2860
+ null,
2861
+ 2
2862
+ )
2863
+ );
2864
+ } else {
2865
+ console.error(`
2866
+ Aborted: ${reason}. Feature "${feature.name}" was NOT deleted.`);
2867
+ for (const t of ticketsFailed) {
2868
+ console.error(` - ${t.shortId ?? t.id}: ${t.error}`);
2869
+ }
2870
+ }
2871
+ process.exitCode = 1;
2872
+ return;
2873
+ }
2874
+ await client.features.delete(feature.id);
2875
+ const ticketsOrphaned = options.orphanTickets ? tickets.map(summarizeTicket) : [];
2876
+ if (useJson) {
2877
+ console.log(
2878
+ JSON.stringify(
2879
+ { deleted: true, id: feature.id, ticketsDeleted, ticketsOrphaned },
2880
+ null,
2881
+ 2
2882
+ )
2883
+ );
2884
+ } else {
2885
+ console.log("\n\u2713 Feature deleted");
2886
+ if (ticketsDeleted.length > 0) {
2887
+ console.log(` ${ticketsDeleted.length} ticket(s) deleted with it`);
2888
+ }
2889
+ if (ticketsOrphaned.length > 0) {
2890
+ console.log(` ${ticketsOrphaned.length} ticket(s) left unlinked in the backlog`);
2891
+ }
2892
+ }
2893
+ } catch (error) {
2894
+ handleError(error, useJson);
2895
+ }
2896
+ }
2897
+ );
2609
2898
  features.command("link-page").description("Link a Knowledge page (PRD, spec, research) to a feature").requiredOption("--feature <id>", "Feature CUID").requiredOption("--page <id>", "Page CUID").option("--scope <id>", "Feature scope CUID to pin the page to").action(
2610
2899
  async (options, cmd) => {
2611
2900
  const globalOpts = cmd.optsWithGlobals();
@@ -3064,8 +3353,86 @@ function createProjectsCommand() {
3064
3353
  return projects;
3065
3354
  }
3066
3355
 
3067
- // src/commands/tickets.ts
3356
+ // src/commands/search.ts
3068
3357
  import { Command as Command16 } from "commander";
3358
+ import chalk6 from "chalk";
3359
+ var TYPE_LABELS = {
3360
+ workspace: "Workspaces",
3361
+ project: "Projects",
3362
+ action: "Actions",
3363
+ goal: "Goals",
3364
+ keyResult: "Key Results",
3365
+ outcome: "Outcomes",
3366
+ ticket: "Tickets",
3367
+ feature: "Features",
3368
+ epic: "Epics",
3369
+ page: "Pages",
3370
+ meeting: "Meetings",
3371
+ contact: "Contacts",
3372
+ organization: "Organizations",
3373
+ product: "Products"
3374
+ };
3375
+ function createSearchCommand() {
3376
+ const search = new Command16("search").description(
3377
+ "Global search across projects, actions, goals, workspaces and more \u2014 same coverage as the app's Cmd+K palette"
3378
+ ).argument("<query>", "Text to search for").option("-w, --workspace <slug>", "Restrict results to one workspace (slug or ID)").option("-l, --limit <n>", "Max results per entity type (1-25, default 10)").action(async (query, options, cmd) => {
3379
+ const globalOpts = cmd.optsWithGlobals();
3380
+ const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
3381
+ try {
3382
+ const client = getClient();
3383
+ let workspaceId;
3384
+ if (options.workspace) {
3385
+ const workspaces = await client.workspaces.list();
3386
+ const match = workspaces.find(
3387
+ (w) => w.slug === options.workspace || w.id === options.workspace
3388
+ );
3389
+ if (!match) {
3390
+ console.error(chalk6.red(`Workspace "${options.workspace}" not found.`));
3391
+ console.log(chalk6.gray("Available workspaces:"));
3392
+ workspaces.forEach((w) => {
3393
+ console.log(chalk6.gray(` - ${w.slug} (${w.name})`));
3394
+ });
3395
+ process.exit(1);
3396
+ }
3397
+ workspaceId = match.id;
3398
+ }
3399
+ const limit = options.limit ? Number(options.limit) : void 0;
3400
+ const result = await client.search.global({ query, workspaceId, limit });
3401
+ if (useJson) {
3402
+ console.log(JSON.stringify(result, null, 2));
3403
+ return;
3404
+ }
3405
+ if (result.results.length === 0) {
3406
+ console.log(chalk6.gray(`No matches for "${result.query}".`));
3407
+ return;
3408
+ }
3409
+ const byType = /* @__PURE__ */ new Map();
3410
+ for (const r of result.results) {
3411
+ const bucket = byType.get(r.type) ?? [];
3412
+ bucket.push(r);
3413
+ byType.set(r.type, bucket);
3414
+ }
3415
+ for (const [type, rows] of byType) {
3416
+ console.log(chalk6.bold(`
3417
+ ${TYPE_LABELS[type] ?? type}`));
3418
+ for (const row of rows) {
3419
+ const parts = [chalk6.white(row.title)];
3420
+ if (row.subtitle) parts.push(chalk6.gray(row.subtitle));
3421
+ if (row.workspace) parts.push(chalk6.cyan(`@${row.workspace.slug}`));
3422
+ console.log(` ${parts.join(chalk6.gray(" \xB7 "))}`);
3423
+ console.log(chalk6.gray(` ${row.id}${row.url ? ` ${row.url}` : ""}`));
3424
+ }
3425
+ }
3426
+ console.log();
3427
+ } catch (error) {
3428
+ handleError(error, useJson);
3429
+ }
3430
+ });
3431
+ return search;
3432
+ }
3433
+
3434
+ // src/commands/tickets.ts
3435
+ import { Command as Command17 } from "commander";
3069
3436
  function collectRepeatable(value, previous) {
3070
3437
  return [...previous, value];
3071
3438
  }
@@ -3108,7 +3475,7 @@ function validateTicketType(value) {
3108
3475
  return value;
3109
3476
  }
3110
3477
  function createTicketsCommand() {
3111
- const tickets = new Command16("tickets").description(
3478
+ const tickets = new Command17("tickets").description(
3112
3479
  "Manage product backlog tickets. Tickets live under a product, link to features/epics, depend on each other, and can be implemented via actions."
3113
3480
  );
3114
3481
  tickets.command("list").description(
@@ -3422,7 +3789,7 @@ function createTicketsCommand() {
3422
3789
  handleError(error, useJson);
3423
3790
  }
3424
3791
  });
3425
- const comment = new Command16("comment").description("Manage comments on a ticket");
3792
+ const comment = new Command17("comment").description("Manage comments on a ticket");
3426
3793
  comment.command("add").description("Add a comment to a ticket").requiredOption("--id <id>", "Ticket CUID").requiredOption("-m, --message <text>", "Comment content (markdown supported)").action(async (options, cmd) => {
3427
3794
  const globalOpts = cmd.optsWithGlobals();
3428
3795
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3461,10 +3828,10 @@ function createTicketsCommand() {
3461
3828
  }
3462
3829
 
3463
3830
  // src/commands/workspaces.ts
3464
- import { Command as Command17 } from "commander";
3465
- import chalk6 from "chalk";
3831
+ import { Command as Command18 } from "commander";
3832
+ import chalk7 from "chalk";
3466
3833
  function createWorkspacesCommand() {
3467
- const workspaces = new Command17("workspaces").description("Manage workspaces");
3834
+ const workspaces = new Command18("workspaces").description("Manage workspaces");
3468
3835
  workspaces.command("list").description("List all workspaces").action(async (_options, cmd) => {
3469
3836
  const globalOpts = cmd.optsWithGlobals();
3470
3837
  const useJson = shouldUseJson(globalOpts.json, globalOpts.pretty);
@@ -3477,7 +3844,7 @@ function createWorkspacesCommand() {
3477
3844
  const config = getConfig();
3478
3845
  outputWorkspacesPretty(workspaces2);
3479
3846
  if (config.defaultWorkspaceSlug) {
3480
- console.log(chalk6.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
3847
+ console.log(chalk7.gray(`Default workspace: ${config.defaultWorkspaceSlug}`));
3481
3848
  }
3482
3849
  }
3483
3850
  } catch (error) {
@@ -3490,10 +3857,10 @@ function createWorkspacesCommand() {
3490
3857
  const workspaces2 = await client.workspaces.list();
3491
3858
  const workspace = workspaces2.find((w) => w.slug === slug);
3492
3859
  if (!workspace) {
3493
- console.error(chalk6.red(`Workspace with slug "${slug}" not found.`));
3494
- console.log(chalk6.gray("Available workspaces:"));
3860
+ console.error(chalk7.red(`Workspace with slug "${slug}" not found.`));
3861
+ console.log(chalk7.gray("Available workspaces:"));
3495
3862
  workspaces2.forEach((w) => {
3496
- console.log(chalk6.gray(` - ${w.slug} (${w.name})`));
3863
+ console.log(chalk7.gray(` - ${w.slug} (${w.name})`));
3497
3864
  });
3498
3865
  process.exit(1);
3499
3866
  }
@@ -3501,7 +3868,7 @@ function createWorkspacesCommand() {
3501
3868
  defaultWorkspaceId: workspace.id,
3502
3869
  defaultWorkspaceSlug: workspace.slug
3503
3870
  });
3504
- console.log(chalk6.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
3871
+ console.log(chalk7.green(`Default workspace set to: ${workspace.name} (${workspace.slug})`));
3505
3872
  } catch (error) {
3506
3873
  handleError(error);
3507
3874
  }
@@ -3510,7 +3877,7 @@ function createWorkspacesCommand() {
3510
3877
  }
3511
3878
 
3512
3879
  // src/index.ts
3513
- var program = new Command18();
3880
+ var program = new Command19();
3514
3881
  program.name("exponential").description(
3515
3882
  [
3516
3883
  "CLI to interact with Exponential productivity app.",
@@ -3531,6 +3898,7 @@ program.addCommand(createOrganizationsCommand());
3531
3898
  program.addCommand(createPagesCommand());
3532
3899
  program.addCommand(createProductsCommand());
3533
3900
  program.addCommand(createProjectsCommand());
3901
+ program.addCommand(createSearchCommand());
3534
3902
  program.addCommand(createTicketsCommand());
3535
3903
  program.addCommand(createWorkspacesCommand());
3536
3904
  program.parse();