ticktick-cli 1.0.2 → 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 +18 -1
- package/dist/auth.js +1 -1
- package/dist/cli.js +192 -4
- 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
|
|
@@ -100,6 +101,8 @@ Interactive login:
|
|
|
100
101
|
ticktick auth login
|
|
101
102
|
```
|
|
102
103
|
|
|
104
|
+
Successful auth stores the token in your local config file and masks secrets in the terminal output by default.
|
|
105
|
+
|
|
103
106
|
If you already have an authorization code:
|
|
104
107
|
|
|
105
108
|
```bash
|
|
@@ -118,6 +121,8 @@ Check current auth state:
|
|
|
118
121
|
ticktick auth status
|
|
119
122
|
```
|
|
120
123
|
|
|
124
|
+
If you need the raw token values in the terminal, add `--show-secrets` to `auth login`, `auth exchange`, or `auth status`.
|
|
125
|
+
|
|
121
126
|
Clear the stored access token:
|
|
122
127
|
|
|
123
128
|
```bash
|
|
@@ -126,6 +131,18 @@ ticktick auth logout
|
|
|
126
131
|
|
|
127
132
|
## Examples
|
|
128
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
|
+
|
|
129
146
|
List projects:
|
|
130
147
|
|
|
131
148
|
```bash
|
package/dist/auth.js
CHANGED
|
@@ -118,7 +118,7 @@ export async function openBrowserWith(url, options = {}) {
|
|
|
118
118
|
const stderr = options.stderr ?? ((text) => process.stderr.write(text));
|
|
119
119
|
try {
|
|
120
120
|
if (platform === "win32") {
|
|
121
|
-
await run("
|
|
121
|
+
await run("rundll32", ["url.dll,FileProtocolHandler", url]);
|
|
122
122
|
return;
|
|
123
123
|
}
|
|
124
124
|
if (platform === "darwin") {
|
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);
|
|
@@ -83,6 +84,7 @@ function buildAuthCommands(root, dependencies) {
|
|
|
83
84
|
.command("login")
|
|
84
85
|
.description("Open the OAuth flow, exchange the code, and store the access token")
|
|
85
86
|
.option("--timeout-ms <number>", "Timeout while waiting for the callback", parseInteger, 120000)
|
|
87
|
+
.option("--show-secrets", "Include access token and refresh token in the output")
|
|
86
88
|
.action(async (...args) => {
|
|
87
89
|
const command = args.at(-1);
|
|
88
90
|
const options = command.optsWithGlobals();
|
|
@@ -97,24 +99,27 @@ function buildAuthCommands(root, dependencies) {
|
|
|
97
99
|
});
|
|
98
100
|
return;
|
|
99
101
|
}
|
|
102
|
+
dependencies.stderr(`Waiting for OAuth callback on ${config.redirectUri}...\n`);
|
|
100
103
|
const codePromise = dependencies.waitForOAuthCode(config.redirectUri, state, options.timeoutMs);
|
|
101
104
|
await dependencies.openBrowser(url);
|
|
102
105
|
const code = await codePromise;
|
|
103
106
|
const token = await dependencies.exchangeAuthorizationCode(config, code);
|
|
104
107
|
await persistConfig(config, token, dependencies);
|
|
105
|
-
dependencies.printJson(token);
|
|
108
|
+
dependencies.printJson(formatAuthSuccess(config, token, options.showSecrets, dependencies));
|
|
106
109
|
});
|
|
107
110
|
auth
|
|
108
111
|
.command("exchange <code>")
|
|
109
112
|
.description("Exchange an authorization code for an access token")
|
|
113
|
+
.option("--show-secrets", "Include access token and refresh token in the output")
|
|
110
114
|
.action(async (...args) => {
|
|
111
115
|
const command = args.at(-1);
|
|
112
116
|
const [code] = args;
|
|
113
|
-
const
|
|
117
|
+
const options = command.optsWithGlobals();
|
|
118
|
+
const config = await dependencies.resolveRuntimeConfig(runtimeOverrides(options, dependencies));
|
|
114
119
|
requireClientCredentials(config);
|
|
115
120
|
const token = await dependencies.exchangeAuthorizationCode(config, code);
|
|
116
121
|
await persistConfig(config, token, dependencies);
|
|
117
|
-
dependencies.printJson(token);
|
|
122
|
+
dependencies.printJson(formatAuthSuccess(config, token, options.showSecrets, dependencies));
|
|
118
123
|
});
|
|
119
124
|
auth
|
|
120
125
|
.command("status")
|
|
@@ -210,8 +215,171 @@ function buildConfigCommands(root, dependencies) {
|
|
|
210
215
|
dependencies.printJson(stored);
|
|
211
216
|
});
|
|
212
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
|
+
}
|
|
213
380
|
function buildTaskCommands(root, dependencies) {
|
|
214
|
-
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);
|
|
215
383
|
task
|
|
216
384
|
.command("get <projectId> <taskId>")
|
|
217
385
|
.description("Get a task by project id and task id")
|
|
@@ -509,6 +677,26 @@ async function persistConfig(runtime, token, dependencies) {
|
|
|
509
677
|
};
|
|
510
678
|
await dependencies.saveStoredConfig(runtime.configFile, next);
|
|
511
679
|
}
|
|
680
|
+
function formatAuthSuccess(runtime, token, showSecrets, dependencies) {
|
|
681
|
+
return {
|
|
682
|
+
ok: true,
|
|
683
|
+
message: "Authorization complete.",
|
|
684
|
+
service: runtime.service,
|
|
685
|
+
configFile: runtime.configFile,
|
|
686
|
+
redirectUri: runtime.redirectUri,
|
|
687
|
+
scope: token.scope ?? runtime.scopes,
|
|
688
|
+
tokenType: token.token_type,
|
|
689
|
+
expiresIn: token.expires_in,
|
|
690
|
+
accessToken: showSecrets
|
|
691
|
+
? token.access_token
|
|
692
|
+
: dependencies.maskSecret(token.access_token),
|
|
693
|
+
refreshToken: token.refresh_token
|
|
694
|
+
? showSecrets
|
|
695
|
+
? token.refresh_token
|
|
696
|
+
: dependencies.maskSecret(token.refresh_token)
|
|
697
|
+
: undefined,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
512
700
|
function requireClientCredentials(config) {
|
|
513
701
|
if (!config.clientId || !config.clientSecret) {
|
|
514
702
|
throw new Error("Client credentials are required. Set TICKTICK_CLIENT_ID and TICKTICK_CLIENT_SECRET or use `ticktick config set`.");
|
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
|
+
}
|