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 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
@@ -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("cmd", ["/c", "start", "", url]);
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 config = await dependencies.resolveRuntimeConfig(runtimeOverrides(command.optsWithGlobals(), dependencies));
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticktick-cli",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "CLI wrapper for the TickTick Open API",
5
5
  "type": "module",
6
6
  "files": [