ticktick-cli 1.0.3 → 1.1.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
@@ -5,7 +5,7 @@ A TypeScript CLI wrapper for the TickTick Open API documented at:
5
5
  - https://developer.ticktick.com/
6
6
  - https://developer.ticktick.com/docs#/openapi
7
7
 
8
- `v1.0.2` covers the documented OAuth flow plus every documented task and project endpoint.
8
+ `v1` covers the documented OAuth flow plus every documented task and project endpoint.
9
9
 
10
10
  The CLI is available as both `ticktick` and the short alias `tt`.
11
11
 
@@ -13,6 +13,7 @@ The CLI is available as both `ticktick` and the short alias `tt`.
13
13
 
14
14
  - OAuth authorization code flow
15
15
  - OAuth authorize URL generation
16
+ - Daily commands for `today`, `overdue`, and `next`
16
17
  - Get, create, update, complete, delete, move, list-completed, and filter task endpoints
17
18
  - List, get, create, update, delete, and get-data project endpoints
18
19
  - Raw authenticated request passthrough
@@ -130,6 +131,18 @@ ticktick auth logout
130
131
 
131
132
  ## Examples
132
133
 
134
+ Daily task views:
135
+
136
+ ```bash
137
+ ticktick today
138
+ tt today
139
+ ticktick overdue
140
+ ticktick next
141
+ ticktick task today --json
142
+ ```
143
+
144
+ `today` includes both overdue tasks and tasks due today, with overdue shown first.
145
+
133
146
  List projects:
134
147
 
135
148
  ```bash
package/dist/cli.js CHANGED
@@ -47,6 +47,7 @@ export function createProgram(dependencies = defaultCliDependencies) {
47
47
  .option("--scopes <scopes>", "Override the OAuth scopes");
48
48
  buildAuthCommands(program, dependencies);
49
49
  buildConfigCommands(program, dependencies);
50
+ buildDailyCommands(program, dependencies);
50
51
  buildTaskCommands(program, dependencies);
51
52
  buildProjectCommands(program, dependencies);
52
53
  buildRequestCommand(program, dependencies);
@@ -214,8 +215,171 @@ function buildConfigCommands(root, dependencies) {
214
215
  dependencies.printJson(stored);
215
216
  });
216
217
  }
218
+ function buildDailyCommands(root, dependencies) {
219
+ buildDailyCommandSet(root, dependencies);
220
+ }
221
+ function buildDailyCommandSet(root, dependencies) {
222
+ root
223
+ .command("today")
224
+ .description("Show overdue tasks and tasks due today")
225
+ .option("--json", "Print structured JSON")
226
+ .action(async (...args) => {
227
+ await runDailyCommand(args, dependencies, "today");
228
+ });
229
+ root
230
+ .command("overdue")
231
+ .description("Show overdue open tasks")
232
+ .option("--json", "Print structured JSON")
233
+ .action(async (...args) => {
234
+ await runDailyCommand(args, dependencies, "overdue");
235
+ });
236
+ root
237
+ .command("next")
238
+ .description("Show the next 10 upcoming tasks after today")
239
+ .option("--json", "Print structured JSON")
240
+ .action(async (...args) => {
241
+ await runDailyCommand(args, dependencies, "next");
242
+ });
243
+ }
244
+ async function runDailyCommand(args, dependencies, mode) {
245
+ const command = args.at(-1);
246
+ const options = command.optsWithGlobals();
247
+ const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
248
+ const client = dependencies.createClient(config);
249
+ const tasks = await client.listOpenTasksWithProjects();
250
+ const dueTasks = collectDueTasks(tasks);
251
+ const bounds = localDayBounds();
252
+ if (mode === "today") {
253
+ const view = buildTodayDueTaskView(dueTasks, bounds.start, bounds.end);
254
+ if (options.json) {
255
+ dependencies.printJson({
256
+ overdue: view.overdue.map(({ task }) => task),
257
+ today: view.today.map(({ task }) => task),
258
+ });
259
+ return;
260
+ }
261
+ printTaskSection("Overdue", view.overdue, "No overdue tasks.");
262
+ process.stdout.write("\n");
263
+ printTaskSection("Today", view.today, "Nothing due today.");
264
+ return;
265
+ }
266
+ const tasksView = mode === "overdue"
267
+ ? buildOverdueDueTaskView(dueTasks, bounds.start)
268
+ : buildNextDueTaskView(dueTasks, bounds.end, 10);
269
+ if (options.json) {
270
+ dependencies.printJson({
271
+ tasks: tasksView.tasks.map(({ task }) => task),
272
+ });
273
+ return;
274
+ }
275
+ if (mode === "overdue") {
276
+ printTaskSection("Overdue", tasksView.tasks, "No overdue tasks.");
277
+ return;
278
+ }
279
+ printTaskSection("Next", tasksView.tasks, "No upcoming tasks after today.");
280
+ }
281
+ function buildTodayDueTaskView(tasks, dayStart, nextDayStart) {
282
+ return {
283
+ overdue: tasks.filter(({ dueAt }) => dueAt.getTime() < dayStart.getTime()),
284
+ today: tasks.filter(({ dueAt }) => dueAt.getTime() >= dayStart.getTime() && dueAt.getTime() < nextDayStart.getTime()),
285
+ };
286
+ }
287
+ function buildOverdueDueTaskView(tasks, dayStart) {
288
+ return {
289
+ tasks: tasks.filter(({ dueAt }) => dueAt.getTime() < dayStart.getTime()),
290
+ };
291
+ }
292
+ function buildNextDueTaskView(tasks, nextDayStart, limit) {
293
+ return {
294
+ tasks: tasks
295
+ .filter(({ dueAt }) => dueAt.getTime() >= nextDayStart.getTime())
296
+ .slice(0, limit),
297
+ };
298
+ }
299
+ function collectDueTasks(tasks) {
300
+ return tasks
301
+ .flatMap((task) => {
302
+ const dueAt = parseTickTickDate(task.dueDate);
303
+ return dueAt ? [{ task, dueAt }] : [];
304
+ })
305
+ .sort(compareDueTasks);
306
+ }
307
+ function compareDueTasks(left, right) {
308
+ const dueTimeDelta = left.dueAt.getTime() - right.dueAt.getTime();
309
+ if (dueTimeDelta !== 0) {
310
+ return dueTimeDelta;
311
+ }
312
+ const priorityDelta = (right.task.priority ?? 0) - (left.task.priority ?? 0);
313
+ if (priorityDelta !== 0) {
314
+ return priorityDelta;
315
+ }
316
+ return (left.task.title ?? "").localeCompare(right.task.title ?? "");
317
+ }
318
+ function parseTickTickDate(value) {
319
+ if (!value) {
320
+ return undefined;
321
+ }
322
+ const normalized = value.replace(/([+-]\d{2})(\d{2})$/, "$1:$2");
323
+ const parsed = new Date(normalized);
324
+ if (Number.isNaN(parsed.getTime())) {
325
+ return undefined;
326
+ }
327
+ return parsed;
328
+ }
329
+ function localDayBounds(reference = new Date()) {
330
+ const start = new Date(reference);
331
+ start.setHours(0, 0, 0, 0);
332
+ const end = new Date(start);
333
+ end.setDate(end.getDate() + 1);
334
+ return { start, end };
335
+ }
336
+ function printTaskSection(title, tasks, emptyMessage) {
337
+ process.stdout.write(`${title} (${tasks.length})\n`);
338
+ if (tasks.length === 0) {
339
+ process.stdout.write(`${emptyMessage}\n`);
340
+ return;
341
+ }
342
+ for (const entry of tasks) {
343
+ process.stdout.write(`${formatTaskLine(entry)}\n`);
344
+ }
345
+ }
346
+ function formatTaskLine(entry) {
347
+ const title = entry.task.title?.trim() || "(Untitled task)";
348
+ const projectName = entry.task.projectName?.trim() || entry.task.projectId || "Unknown project";
349
+ return `- ${title} [${projectName}] - ${formatDueLabel(entry)}`;
350
+ }
351
+ function formatDueLabel(entry) {
352
+ const bounds = localDayBounds();
353
+ const due = entry.dueAt;
354
+ const dayKey = dueDayKey(due);
355
+ const todayKey = dueDayKey(bounds.start);
356
+ const yesterday = new Date(bounds.start);
357
+ yesterday.setDate(yesterday.getDate() - 1);
358
+ const yesterdayKey = dueDayKey(yesterday);
359
+ const baseLabel = dayKey === todayKey
360
+ ? "Today"
361
+ : dayKey === yesterdayKey
362
+ ? "Yesterday"
363
+ : new Intl.DateTimeFormat(undefined, {
364
+ weekday: "short",
365
+ month: "short",
366
+ day: "numeric",
367
+ }).format(due);
368
+ if (entry.task.isAllDay) {
369
+ return `${baseLabel} (all day)`;
370
+ }
371
+ const timeLabel = new Intl.DateTimeFormat(undefined, {
372
+ hour: "numeric",
373
+ minute: "2-digit",
374
+ }).format(due);
375
+ return `${baseLabel} ${timeLabel}`;
376
+ }
377
+ function dueDayKey(value) {
378
+ return `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}`;
379
+ }
217
380
  function buildTaskCommands(root, dependencies) {
218
- const task = root.command("task").description("Task endpoints");
381
+ const task = root.command("task").description("Task endpoints and daily task views");
382
+ buildDailyCommandSet(task, dependencies);
219
383
  task
220
384
  .command("get <projectId> <taskId>")
221
385
  .description("Get a task by project id and task id")
package/dist/client.js CHANGED
@@ -72,6 +72,30 @@ export class TickTickClient {
72
72
  filterTasks(payload) {
73
73
  return this.requestRaw("POST", "/open/v1/task/filter", payload);
74
74
  }
75
+ async listOpenTasksWithProjects() {
76
+ const tasks = ((await this.filterTasks({ status: [0] })) ?? []).filter(isOpenTask);
77
+ const projectNames = new Map();
78
+ const projects = (await this.listProjects()) ?? [];
79
+ for (const project of projects) {
80
+ if (project.id && project.name) {
81
+ projectNames.set(project.id, project.name);
82
+ }
83
+ }
84
+ const missingProjectIds = Array.from(new Set(tasks
85
+ .map((task) => task.projectId)
86
+ .filter((projectId) => Boolean(projectId && !projectNames.has(projectId)))));
87
+ await Promise.all(missingProjectIds.map(async (projectId) => {
88
+ const data = await this.getProjectData(projectId);
89
+ const projectName = data?.project?.name ?? inferProjectName(projectId);
90
+ if (projectName) {
91
+ projectNames.set(projectId, projectName);
92
+ }
93
+ }));
94
+ return tasks.map((task) => ({
95
+ ...task,
96
+ projectName: task.projectId ? projectNames.get(task.projectId) ?? inferProjectName(task.projectId) : undefined,
97
+ }));
98
+ }
75
99
  listProjects() {
76
100
  return this.requestRaw("GET", "/open/v1/project");
77
101
  }
@@ -99,3 +123,15 @@ function tryParseJson(value) {
99
123
  return value;
100
124
  }
101
125
  }
126
+ function isOpenTask(task) {
127
+ return !task.completedTime && task.status !== 2;
128
+ }
129
+ function inferProjectName(projectId) {
130
+ if (!projectId) {
131
+ return undefined;
132
+ }
133
+ if (projectId.startsWith("inbox")) {
134
+ return "Inbox";
135
+ }
136
+ return undefined;
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticktick-cli",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "CLI wrapper for the TickTick Open API",
5
5
  "type": "module",
6
6
  "files": [