@mindtnv/todoist-cli 0.4.0 → 0.5.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/package.json +6 -6
- package/src/api/activity.ts +8 -0
- package/src/api/client.ts +214 -0
- package/src/api/comments.ts +18 -0
- package/src/api/completed.ts +15 -0
- package/src/api/labels.ts +18 -0
- package/src/api/projects.ts +22 -0
- package/src/api/sections.ts +20 -0
- package/src/api/stats.ts +38 -0
- package/src/api/tasks.ts +34 -0
- package/src/api/types.ts +202 -0
- package/src/cli/auth.ts +40 -0
- package/src/cli/commands/task/add.ts +328 -0
- package/src/cli/commands/task/complete.ts +62 -0
- package/src/cli/commands/task/delete.ts +62 -0
- package/src/cli/commands/task/helpers.ts +289 -0
- package/src/cli/commands/task/index.ts +27 -0
- package/src/cli/commands/task/list.ts +151 -0
- package/src/cli/commands/task/move.ts +49 -0
- package/src/cli/commands/task/reopen.ts +43 -0
- package/src/cli/commands/task/show.ts +115 -0
- package/src/cli/commands/task/update.ts +122 -0
- package/src/cli/comment.ts +83 -0
- package/src/cli/completed.ts +87 -0
- package/src/cli/completion.ts +360 -0
- package/src/cli/filter.ts +115 -0
- package/src/cli/index.ts +638 -0
- package/src/cli/label.ts +120 -0
- package/src/cli/log.ts +57 -0
- package/src/cli/matrix.ts +100 -0
- package/src/cli/plugin-loader.ts +38 -0
- package/src/cli/plugin.ts +289 -0
- package/src/cli/project.ts +172 -0
- package/src/cli/review.ts +116 -0
- package/src/cli/section.ts +98 -0
- package/src/cli/stats.ts +62 -0
- package/src/cli/template.ts +89 -0
- package/src/config/index.ts +229 -0
- package/src/plugins/api-proxy.ts +70 -0
- package/src/plugins/extension-registry.ts +53 -0
- package/src/plugins/hook-registry.ts +36 -0
- package/src/plugins/loader.ts +200 -0
- package/src/plugins/marketplace-types.ts +55 -0
- package/src/plugins/marketplace.ts +576 -0
- package/src/plugins/palette-registry.ts +21 -0
- package/src/plugins/storage.ts +101 -0
- package/src/plugins/types.ts +226 -0
- package/src/plugins/view-registry.ts +19 -0
- package/src/ui/App.tsx +234 -0
- package/src/ui/components/Breadcrumb.tsx +18 -0
- package/src/ui/components/CommandPalette.tsx +237 -0
- package/src/ui/components/ConfirmDialog.tsx +28 -0
- package/src/ui/components/EditTaskModal.tsx +484 -0
- package/src/ui/components/HelpOverlay.tsx +195 -0
- package/src/ui/components/InputPrompt.tsx +109 -0
- package/src/ui/components/LabelPicker.tsx +110 -0
- package/src/ui/components/ModalManager.tsx +275 -0
- package/src/ui/components/ProjectPicker.tsx +95 -0
- package/src/ui/components/Sidebar.tsx +282 -0
- package/src/ui/components/SortMenu.tsx +77 -0
- package/src/ui/components/StatusBar.tsx +67 -0
- package/src/ui/components/TaskList.tsx +258 -0
- package/src/ui/components/TaskRow.tsx +105 -0
- package/src/ui/hooks/useKeyboardHandler.ts +291 -0
- package/src/ui/hooks/useStatusMessage.ts +32 -0
- package/src/ui/hooks/useTaskOperations.ts +558 -0
- package/src/ui/hooks/useUndoSystem.ts +218 -0
- package/src/ui/views/ActivityView.tsx +213 -0
- package/src/ui/views/CompletedView.tsx +337 -0
- package/src/ui/views/StatsView.tsx +178 -0
- package/src/ui/views/TaskDetailView.tsx +438 -0
- package/src/ui/views/TasksView.tsx +851 -0
- package/src/utils/colors.ts +27 -0
- package/src/utils/date-format.ts +54 -0
- package/src/utils/errors.ts +159 -0
- package/src/utils/exit.ts +11 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/open-url.ts +9 -0
- package/src/utils/output.ts +29 -0
- package/src/utils/quick-add.ts +202 -0
- package/src/utils/resolve.ts +359 -0
- package/src/utils/sorting.ts +27 -0
- package/src/utils/validation.ts +88 -0
- package/dist/index.js +0 -11355
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindtnv/todoist-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A fast, keyboard-driven Todoist client for the terminal — interactive TUI and scriptable CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"todoist": "
|
|
7
|
+
"todoist": "src/cli/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
10
|
+
"src",
|
|
11
11
|
"marketplace.json"
|
|
12
12
|
],
|
|
13
13
|
"scripts": {
|
|
14
|
-
"dev": "bun run src/cli/index.ts",
|
|
15
|
-
"build": "bun build src/cli/index.ts --outdir dist --target
|
|
14
|
+
"dev": "TODOIST_CLI_CONFIG_DIR=$HOME/.config/todoist-cli-dev bun run src/cli/index.ts",
|
|
15
|
+
"build": "bun build src/cli/index.ts --outdir dist --target bun --packages external --external react-devtools-core",
|
|
16
16
|
"prepublishOnly": "bun run build",
|
|
17
17
|
"test": "bun test"
|
|
18
18
|
},
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"homepage": "https://github.com/mindtnv/todoist-cli#readme",
|
|
39
39
|
"engines": {
|
|
40
|
-
"
|
|
40
|
+
"bun": ">=1.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@iarna/toml": "^2.2.5",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { api } from "./client.ts";
|
|
2
|
+
import type { ActivityEvent } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function getActivity(limit?: number): Promise<ActivityEvent[]> {
|
|
5
|
+
const params: Record<string, string> = {};
|
|
6
|
+
if (limit) params.limit = String(limit);
|
|
7
|
+
return api.get<ActivityEvent[]>("/activities", params);
|
|
8
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { requireToken } from "../config/index.ts";
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://api.todoist.com/api/v1";
|
|
4
|
+
|
|
5
|
+
const MAX_RETRIES = 3;
|
|
6
|
+
const BASE_BACKOFF_MS = 1000;
|
|
7
|
+
const RATE_LIMIT_BASE_BACKOFF_MS = 5000;
|
|
8
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
9
|
+
const JITTER_FACTOR = 0.25;
|
|
10
|
+
|
|
11
|
+
interface PaginatedResponse<T> {
|
|
12
|
+
results: T[];
|
|
13
|
+
next_cursor: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sleep(ms: number): Promise<void> {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Add ±25% jitter to a delay value. */
|
|
21
|
+
function addJitter(ms: number): number {
|
|
22
|
+
const jitter = ms * JITTER_FACTOR * (2 * Math.random() - 1);
|
|
23
|
+
return Math.max(0, ms + jitter);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Check if an error is a network/connectivity error worth retrying. */
|
|
27
|
+
function isNetworkError(err: unknown): boolean {
|
|
28
|
+
if (err instanceof TypeError) return true;
|
|
29
|
+
if (err instanceof Error && err.message.includes("fetch failed")) return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Check if an error is a timeout abort. */
|
|
34
|
+
function isTimeoutError(err: unknown): boolean {
|
|
35
|
+
if (err instanceof DOMException && err.name === "AbortError") return true;
|
|
36
|
+
if (err instanceof Error && err.name === "AbortError") return true;
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class TodoistClient {
|
|
41
|
+
private get headers(): Record<string, string> {
|
|
42
|
+
return {
|
|
43
|
+
Authorization: `Bearer ${requireToken()}`,
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async get<T>(path: string, params?: Record<string, string>): Promise<T> {
|
|
49
|
+
let url = `${BASE_URL}${path}`;
|
|
50
|
+
if (params) {
|
|
51
|
+
const query = new URLSearchParams(params).toString();
|
|
52
|
+
if (query) url += `?${query}`;
|
|
53
|
+
}
|
|
54
|
+
const res = await this.fetchWithRetry(url, { headers: this.headers });
|
|
55
|
+
const data = await this.handleResponse<T | PaginatedResponse<unknown>>(res);
|
|
56
|
+
if (data && typeof data === "object" && "results" in data && "next_cursor" in data) {
|
|
57
|
+
return (data as PaginatedResponse<unknown>).results as T;
|
|
58
|
+
}
|
|
59
|
+
return data as T;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async post<T>(path: string, body?: Record<string, unknown>): Promise<T> {
|
|
63
|
+
const res = await this.fetchWithRetry(`${BASE_URL}${path}`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: this.headers,
|
|
66
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
67
|
+
});
|
|
68
|
+
return this.handleResponse<T>(res);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async del(path: string): Promise<void> {
|
|
72
|
+
const res = await this.fetchWithRetry(`${BASE_URL}${path}`, {
|
|
73
|
+
method: "DELETE",
|
|
74
|
+
headers: this.headers,
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) await this.throwError(res);
|
|
77
|
+
await res.text();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetch with retry, exponential backoff, rate-limit handling, and timeout.
|
|
82
|
+
* Retries on: network errors, timeouts, 5xx responses, and 429 rate limits.
|
|
83
|
+
* Does NOT retry on 4xx errors (except 429).
|
|
84
|
+
*/
|
|
85
|
+
private async fetchWithRetry(url: string, init: RequestInit): Promise<Response> {
|
|
86
|
+
let lastError: Error | undefined;
|
|
87
|
+
let lastRetryAfter: number | undefined;
|
|
88
|
+
let skipNextBackoff = false;
|
|
89
|
+
|
|
90
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
91
|
+
// Wait before retrying (skip delay on first attempt and after 429 which has its own delay)
|
|
92
|
+
if (attempt > 0 && !skipNextBackoff) {
|
|
93
|
+
const backoff = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
|
|
94
|
+
const delay = addJitter(backoff);
|
|
95
|
+
process.stderr.write(
|
|
96
|
+
`[todoist-cli] Retry ${attempt}/${MAX_RETRIES} in ${Math.round(delay)}ms...\n`,
|
|
97
|
+
);
|
|
98
|
+
await sleep(delay);
|
|
99
|
+
}
|
|
100
|
+
skipNextBackoff = false;
|
|
101
|
+
|
|
102
|
+
// Set up per-request timeout via AbortController
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
105
|
+
const fetchInit: RequestInit = {
|
|
106
|
+
...init,
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let res: Response;
|
|
111
|
+
try {
|
|
112
|
+
res = await fetch(url, fetchInit);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
clearTimeout(timeoutId);
|
|
115
|
+
|
|
116
|
+
if (isTimeoutError(err)) {
|
|
117
|
+
lastError = new Error(`Request timed out after 30s`);
|
|
118
|
+
continue; // retry on timeout
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isNetworkError(err)) {
|
|
122
|
+
lastError = new Error(
|
|
123
|
+
"Network error: unable to reach Todoist API. Check your connection.",
|
|
124
|
+
);
|
|
125
|
+
continue; // retry on network error
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw err; // unknown error, don't retry
|
|
129
|
+
} finally {
|
|
130
|
+
clearTimeout(timeoutId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Success — return response
|
|
134
|
+
if (res.ok) {
|
|
135
|
+
return res;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 429 Rate Limit — retry with Retry-After header or rate-limit backoff
|
|
139
|
+
if (res.status === 429) {
|
|
140
|
+
const retryAfterHeader = res.headers.get("Retry-After");
|
|
141
|
+
let waitMs: number;
|
|
142
|
+
|
|
143
|
+
if (retryAfterHeader) {
|
|
144
|
+
const retryAfterSec = parseInt(retryAfterHeader, 10);
|
|
145
|
+
waitMs = (Number.isNaN(retryAfterSec) ? RATE_LIMIT_BASE_BACKOFF_MS / 1000 : retryAfterSec) * 1000;
|
|
146
|
+
} else {
|
|
147
|
+
waitMs = RATE_LIMIT_BASE_BACKOFF_MS * Math.pow(2, attempt);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
lastRetryAfter = Math.ceil(waitMs / 1000);
|
|
151
|
+
|
|
152
|
+
if (attempt < MAX_RETRIES) {
|
|
153
|
+
const delay = addJitter(waitMs);
|
|
154
|
+
process.stderr.write(
|
|
155
|
+
`[todoist-cli] Rate limited (429). Retry ${attempt + 1}/${MAX_RETRIES} in ${Math.round(delay)}ms...\n`,
|
|
156
|
+
);
|
|
157
|
+
await sleep(delay);
|
|
158
|
+
skipNextBackoff = true; // already waited with rate-limit-specific delay
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Rate limit exceeded. Retried ${MAX_RETRIES} times. Try again in ${lastRetryAfter} seconds.`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 5xx Server Error — retry
|
|
168
|
+
if (res.status >= 500) {
|
|
169
|
+
lastError = new Error(`API error ${res.status}: ${res.statusText}`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 4xx Client Error (non-429) — do NOT retry, return for throwError handling
|
|
174
|
+
return res;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// All retries exhausted
|
|
178
|
+
throw lastError ?? new Error("Request failed after all retries.");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async handleResponse<T>(res: Response): Promise<T> {
|
|
182
|
+
if (!res.ok) await this.throwError(res);
|
|
183
|
+
const text = await res.text();
|
|
184
|
+
if (!text) return undefined as T;
|
|
185
|
+
return JSON.parse(text) as T;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async throwError(res: Response): Promise<never> {
|
|
189
|
+
if (res.status === 401) {
|
|
190
|
+
throw new Error("Authentication failed. Run `todoist auth` to set a valid API token.");
|
|
191
|
+
}
|
|
192
|
+
if (res.status === 429) {
|
|
193
|
+
throw new Error("Rate limit exceeded. Please wait and try again.");
|
|
194
|
+
}
|
|
195
|
+
let detail = res.statusText;
|
|
196
|
+
try {
|
|
197
|
+
const body = await res.text();
|
|
198
|
+
if (body) {
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(body);
|
|
201
|
+
if (parsed.error) detail = parsed.error;
|
|
202
|
+
else detail = body;
|
|
203
|
+
} catch {
|
|
204
|
+
detail = body;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// use statusText as fallback
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`API error ${res.status}: ${detail}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export const api = new TodoistClient();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { api } from "./client.ts";
|
|
2
|
+
import type { Comment, CreateCommentParams, UpdateCommentParams } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function getComments(taskId: string): Promise<Comment[]> {
|
|
5
|
+
return api.get<Comment[]>("/comments", { task_id: taskId });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function createComment(params: CreateCommentParams): Promise<Comment> {
|
|
9
|
+
return api.post<Comment>("/comments", params as unknown as Record<string, unknown>);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function updateComment(id: string, params: UpdateCommentParams): Promise<Comment> {
|
|
13
|
+
return api.post<Comment>(`/comments/${id}`, params as unknown as Record<string, unknown>);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function deleteComment(id: string): Promise<void> {
|
|
17
|
+
await api.del(`/comments/${id}`);
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { api } from "./client.ts";
|
|
2
|
+
import type { CompletedTask } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
interface CompletedResponse {
|
|
5
|
+
items: CompletedTask[];
|
|
6
|
+
projects: Record<string, unknown>;
|
|
7
|
+
sections: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function getCompletedTasks(since?: string): Promise<CompletedTask[]> {
|
|
11
|
+
const params: Record<string, string> = {};
|
|
12
|
+
if (since) params.since = since;
|
|
13
|
+
const data = await api.get<CompletedResponse>("/tasks/completed", params);
|
|
14
|
+
return data.items ?? [];
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { api } from "./client.ts";
|
|
2
|
+
import type { Label, CreateLabelParams, UpdateLabelParams } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function getLabels(): Promise<Label[]> {
|
|
5
|
+
return api.get<Label[]>("/labels");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function createLabel(params: CreateLabelParams): Promise<Label> {
|
|
9
|
+
return api.post<Label>("/labels", params as unknown as Record<string, unknown>);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function updateLabel(id: string, params: UpdateLabelParams): Promise<Label> {
|
|
13
|
+
return api.post<Label>(`/labels/${id}`, params as unknown as Record<string, unknown>);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function deleteLabel(id: string): Promise<void> {
|
|
17
|
+
await api.del(`/labels/${id}`);
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { api } from "./client.ts";
|
|
2
|
+
import type { Project, CreateProjectParams, UpdateProjectParams } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function getProjects(): Promise<Project[]> {
|
|
5
|
+
return api.get<Project[]>("/projects");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function getProject(id: string): Promise<Project> {
|
|
9
|
+
return api.get<Project>(`/projects/${id}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function createProject(params: CreateProjectParams): Promise<Project> {
|
|
13
|
+
return api.post<Project>("/projects", params as unknown as Record<string, unknown>);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function updateProject(id: string, params: UpdateProjectParams): Promise<Project> {
|
|
17
|
+
return api.post<Project>(`/projects/${id}`, params as unknown as Record<string, unknown>);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function deleteProject(id: string): Promise<void> {
|
|
21
|
+
await api.del(`/projects/${id}`);
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { api } from "./client.ts";
|
|
2
|
+
import type { Section, CreateSectionParams, UpdateSectionParams } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function getSections(projectId?: string): Promise<Section[]> {
|
|
5
|
+
const params: Record<string, string> = {};
|
|
6
|
+
if (projectId) params.project_id = projectId;
|
|
7
|
+
return api.get<Section[]>("/sections", params);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function createSection(params: CreateSectionParams): Promise<Section> {
|
|
11
|
+
return api.post<Section>("/sections", params as unknown as Record<string, unknown>);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function updateSection(id: string, params: UpdateSectionParams): Promise<Section> {
|
|
15
|
+
return api.post<Section>(`/sections/${id}`, params as unknown as Record<string, unknown>);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function deleteSection(id: string): Promise<void> {
|
|
19
|
+
await api.del(`/sections/${id}`);
|
|
20
|
+
}
|
package/src/api/stats.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { api } from "./client.ts";
|
|
2
|
+
import type { UserStats, DayStats, WeekStats } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
interface UserResponse {
|
|
5
|
+
karma: number;
|
|
6
|
+
karma_trend: string;
|
|
7
|
+
completed_today: number;
|
|
8
|
+
completed_count: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SyncStatsResponse {
|
|
12
|
+
stats?: {
|
|
13
|
+
completed_count: number;
|
|
14
|
+
days_items: Array<{ date: string; total_completed: number }>;
|
|
15
|
+
week_items: Array<{ from: string; to: string; total_completed: number }>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getStats(): Promise<UserStats> {
|
|
20
|
+
const [user, syncData] = await Promise.all([
|
|
21
|
+
api.get<UserResponse>("/user"),
|
|
22
|
+
api.post<SyncStatsResponse>("/sync", {
|
|
23
|
+
resource_types: ["stats"],
|
|
24
|
+
sync_token: "*",
|
|
25
|
+
}),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const stats = syncData.stats;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
completed_count: user.completed_count,
|
|
32
|
+
karma: user.karma,
|
|
33
|
+
karma_trend: user.karma_trend,
|
|
34
|
+
completed_today: user.completed_today,
|
|
35
|
+
days_items: (stats?.days_items ?? []) as DayStats[],
|
|
36
|
+
week_items: (stats?.week_items ?? []) as WeekStats[],
|
|
37
|
+
};
|
|
38
|
+
}
|
package/src/api/tasks.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { api } from "./client.ts";
|
|
2
|
+
import type { Task, TaskFilter, CreateTaskParams, UpdateTaskParams } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function getTasks(filter?: TaskFilter): Promise<Task[]> {
|
|
5
|
+
const params: Record<string, string> = {};
|
|
6
|
+
if (filter?.project_id) params.project_id = filter.project_id;
|
|
7
|
+
if (filter?.label) params.label = filter.label;
|
|
8
|
+
if (filter?.filter) params.filter = filter.filter;
|
|
9
|
+
return api.get<Task[]>("/tasks", params);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function getTask(id: string): Promise<Task> {
|
|
13
|
+
return api.get<Task>(`/tasks/${id}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function createTask(params: CreateTaskParams): Promise<Task> {
|
|
17
|
+
return api.post<Task>("/tasks", params as unknown as Record<string, unknown>);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function updateTask(id: string, params: UpdateTaskParams): Promise<Task> {
|
|
21
|
+
return api.post<Task>(`/tasks/${id}`, params as unknown as Record<string, unknown>);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function closeTask(id: string): Promise<void> {
|
|
25
|
+
await api.post<void>(`/tasks/${id}/close`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function reopenTask(id: string): Promise<void> {
|
|
29
|
+
await api.post<void>(`/tasks/${id}/reopen`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function deleteTask(id: string): Promise<void> {
|
|
33
|
+
await api.del(`/tasks/${id}`);
|
|
34
|
+
}
|
package/src/api/types.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
export type Priority = 1 | 2 | 3 | 4;
|
|
2
|
+
|
|
3
|
+
export interface Deadline {
|
|
4
|
+
date: string; // YYYY-MM-DD
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Task {
|
|
8
|
+
id: string;
|
|
9
|
+
content: string;
|
|
10
|
+
description: string;
|
|
11
|
+
project_id: string;
|
|
12
|
+
section_id: string | null;
|
|
13
|
+
parent_id: string | null;
|
|
14
|
+
order: number;
|
|
15
|
+
priority: Priority;
|
|
16
|
+
due: Due | null;
|
|
17
|
+
deadline: Deadline | null;
|
|
18
|
+
labels: string[];
|
|
19
|
+
assignee_id: string | null;
|
|
20
|
+
is_completed: boolean;
|
|
21
|
+
created_at: string;
|
|
22
|
+
comment_count: number;
|
|
23
|
+
creator_id: string;
|
|
24
|
+
url: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Due {
|
|
28
|
+
date: string;
|
|
29
|
+
string: string;
|
|
30
|
+
lang: string;
|
|
31
|
+
is_recurring: boolean;
|
|
32
|
+
datetime: string | null;
|
|
33
|
+
timezone: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Project {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
color: string;
|
|
40
|
+
parent_id: string | null;
|
|
41
|
+
order: number;
|
|
42
|
+
comment_count: number;
|
|
43
|
+
is_shared: boolean;
|
|
44
|
+
is_favorite: boolean;
|
|
45
|
+
is_inbox_project: boolean;
|
|
46
|
+
view_style: "list" | "board";
|
|
47
|
+
url: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Label {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
color: string;
|
|
54
|
+
order: number;
|
|
55
|
+
is_favorite: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface Comment {
|
|
59
|
+
id: string;
|
|
60
|
+
task_id: string;
|
|
61
|
+
content: string;
|
|
62
|
+
posted_at: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface Section {
|
|
66
|
+
id: string;
|
|
67
|
+
project_id: string;
|
|
68
|
+
order: number;
|
|
69
|
+
name: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CreateTaskParams {
|
|
73
|
+
content: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
project_id?: string;
|
|
76
|
+
priority?: Priority;
|
|
77
|
+
due_string?: string;
|
|
78
|
+
due_date?: string;
|
|
79
|
+
deadline_date?: string | null;
|
|
80
|
+
labels?: string[];
|
|
81
|
+
parent_id?: string;
|
|
82
|
+
section_id?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface UpdateTaskParams {
|
|
86
|
+
content?: string;
|
|
87
|
+
description?: string;
|
|
88
|
+
priority?: Priority;
|
|
89
|
+
due_string?: string;
|
|
90
|
+
due_date?: string;
|
|
91
|
+
deadline_date?: string | null;
|
|
92
|
+
labels?: string[];
|
|
93
|
+
project_id?: string;
|
|
94
|
+
section_id?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface UpdateProjectParams {
|
|
98
|
+
name?: string;
|
|
99
|
+
color?: string;
|
|
100
|
+
is_favorite?: boolean;
|
|
101
|
+
view_style?: "list" | "board";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface UpdateLabelParams {
|
|
105
|
+
name?: string;
|
|
106
|
+
color?: string;
|
|
107
|
+
order?: number;
|
|
108
|
+
is_favorite?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface UpdateSectionParams {
|
|
112
|
+
name?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface UpdateCommentParams {
|
|
116
|
+
content?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface CreateProjectParams {
|
|
120
|
+
name: string;
|
|
121
|
+
color?: string;
|
|
122
|
+
parent_id?: string;
|
|
123
|
+
view_style?: "list" | "board";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CreateLabelParams {
|
|
127
|
+
name: string;
|
|
128
|
+
color?: string;
|
|
129
|
+
order?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface CreateCommentParams {
|
|
133
|
+
task_id: string;
|
|
134
|
+
content: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface TaskFilter {
|
|
138
|
+
project_id?: string;
|
|
139
|
+
label?: string;
|
|
140
|
+
filter?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ApiError {
|
|
144
|
+
error: string;
|
|
145
|
+
http_code: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface TaskTemplate {
|
|
149
|
+
name: string;
|
|
150
|
+
content: string;
|
|
151
|
+
description?: string;
|
|
152
|
+
priority?: Priority;
|
|
153
|
+
labels?: string[];
|
|
154
|
+
due_string?: string;
|
|
155
|
+
deadline_date?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface CreateSectionParams {
|
|
159
|
+
name: string;
|
|
160
|
+
project_id: string;
|
|
161
|
+
order?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface CompletedTask {
|
|
165
|
+
id: string;
|
|
166
|
+
task_id: string;
|
|
167
|
+
content: string;
|
|
168
|
+
project_id: string;
|
|
169
|
+
section_id: string | null;
|
|
170
|
+
completed_at: string;
|
|
171
|
+
user_id: string;
|
|
172
|
+
note_count: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ActivityEvent {
|
|
176
|
+
id: string;
|
|
177
|
+
object_type: string;
|
|
178
|
+
object_id: string;
|
|
179
|
+
event_type: string;
|
|
180
|
+
event_date: string;
|
|
181
|
+
extra_data: Record<string, unknown>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface UserStats {
|
|
185
|
+
completed_count: number;
|
|
186
|
+
completed_today: number;
|
|
187
|
+
karma: number;
|
|
188
|
+
karma_trend: string;
|
|
189
|
+
days_items: DayStats[];
|
|
190
|
+
week_items: WeekStats[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface DayStats {
|
|
194
|
+
date: string;
|
|
195
|
+
total_completed: number;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface WeekStats {
|
|
199
|
+
from: string;
|
|
200
|
+
to: string;
|
|
201
|
+
total_completed: number;
|
|
202
|
+
}
|
package/src/cli/auth.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { setToken } from "../config/index.ts";
|
|
4
|
+
import { cliExit } from "../utils/exit.ts";
|
|
5
|
+
import { getProjects } from "../api/projects.ts";
|
|
6
|
+
import { createInterface } from "readline";
|
|
7
|
+
|
|
8
|
+
export function registerAuthCommand(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command("auth")
|
|
11
|
+
.description("Authenticate with your Todoist API token")
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const rl = createInterface({
|
|
14
|
+
input: process.stdin,
|
|
15
|
+
output: process.stdout,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const token = await new Promise<string>((resolve) => {
|
|
19
|
+
rl.question("Enter your Todoist API token: ", (answer) => {
|
|
20
|
+
rl.close();
|
|
21
|
+
resolve(answer.trim());
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!token) {
|
|
26
|
+
console.error(chalk.red("Token cannot be empty."));
|
|
27
|
+
cliExit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setToken(token);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const projects = await getProjects();
|
|
34
|
+
console.log(chalk.green(`Authenticated successfully. Found ${projects.length} project(s).`));
|
|
35
|
+
} catch {
|
|
36
|
+
console.error(chalk.red("Authentication failed. The token may be invalid."));
|
|
37
|
+
cliExit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|