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 +14 -1
- package/dist/cli.js +165 -1
- package/dist/client.js +36 -0
- package/package.json +1 -1
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
|
|
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
|
+
}
|