@kaname-tasks/kaname 0.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 ADDED
@@ -0,0 +1,128 @@
1
+ # Kaname CLI
2
+
3
+ Command-line client for Kaname. See `docs/cli-plan.md` for the design.
4
+
5
+ The package ships two binaries: `kaname` (the CLI) and `kaname-mcp` (an MCP
6
+ server exposing the same operations as tools — see [MCP server](#mcp-server)).
7
+
8
+ ## Install (from the monorepo)
9
+
10
+ ```bash
11
+ cd cli
12
+ npm install
13
+ npm run build
14
+ npm link # makes `kaname` and `kaname-mcp` available on your PATH
15
+ ```
16
+
17
+ ## Login
18
+
19
+ ```bash
20
+ kaname login # Google sign-in via your browser (default)
21
+ kaname login --password # email/password prompt
22
+ kaname login --api-url https://api.example.com # persist a backend URL
23
+ ```
24
+
25
+ Google login needs a "Desktop app" OAuth client from the same Google Cloud
26
+ project as the web app, provided via `KANAME_GOOGLE_CLIENT_ID` /
27
+ `KANAME_GOOGLE_CLIENT_SECRET` (or `googleClientId` / `googleClientSecret` in
28
+ `~/.config/kaname/config.json`). The backend must list that client id in
29
+ `GOOGLE_CLI_CLIENT_ID`.
30
+
31
+ Tokens are stored in `~/.config/kaname/credentials.json` (mode 0600) and
32
+ refresh automatically; you only re-login after 30+ days of inactivity.
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ kaname add buy milk --list groceries --when today
38
+ kaname add call mom --when fri --due +7d --notes "she prefers evenings"
39
+
40
+ kaname ls # today view (default)
41
+ kaname ls --inbox # todos with no list
42
+ kaname ls --all
43
+ kaname ls --list groceries
44
+ kaname ls --smart errands
45
+
46
+ kaname show 1 # #-indexes refer to the last `kaname ls` output
47
+ kaname done 1 2 # complete several at once
48
+ kaname undone 2
49
+ kaname edit 1 --when tomorrow --list none
50
+ kaname rm 1 # asks for confirmation (or --force)
51
+
52
+ kaname subtask add 1 whole milk
53
+ kaname subtask done 1 1
54
+ kaname subtask rm 1 2
55
+
56
+ kaname list ls
57
+ kaname list add groceries --color '#22c55e'
58
+ kaname list rename groceries errands
59
+ kaname list rm errands
60
+
61
+ kaname whoami
62
+ kaname logout
63
+ ```
64
+
65
+ Dates accept `today`, `tomorrow`, weekday names (`fri`), offsets (`+3d`), ISO
66
+ (`2026-07-10`), and `none` to clear a date in `edit`.
67
+
68
+ Every read command takes `--json` (raw API response, for `jq`/scripts) and
69
+ `--plain` (tab-separated, no color). Exit codes: `1` for errors, `2` for
70
+ "not logged in / session expired".
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ npm run dev -- ls # run from source
76
+ npm run lint # typecheck
77
+ npm test # unit tests
78
+ npm run generate-types # regenerate src/api/types.ts from a running backend
79
+ ```
80
+
81
+ `src/api/types.ts` is generated from the backend OpenAPI spec and committed.
82
+ Regenerate it after changing any Pydantic schema (same rule as the frontend).
83
+
84
+ ## MCP server
85
+
86
+ `kaname-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)
87
+ server that lets an AI assistant (Claude Desktop, Claude Code, …) read and
88
+ manage your kaname todos. It's a stdio server that reuses the CLI's stored
89
+ session — so **log in once with `kaname login`**, and the MCP server picks up
90
+ the same credentials (with automatic token refresh). No separate auth.
91
+
92
+ ### Tools
93
+
94
+ Full parity with the CLI:
95
+
96
+ - Todos: `list_todos`, `get_todo`, `add_todo`, `update_todo`, `complete_todo`,
97
+ `reopen_todo`, `delete_todo`
98
+ - Subtasks: `add_subtask`, `update_subtask`, `delete_subtask`
99
+ - Lists: `list_lists`, `list_smart_lists`, `create_list`, `rename_list`,
100
+ `delete_list`
101
+
102
+ Todos and subtasks are addressed by id (uuid); `list_todos` returns those ids.
103
+ Lists accept either a name or an id.
104
+
105
+ ### Connect it
106
+
107
+ **Claude Code:**
108
+
109
+ ```bash
110
+ claude mcp add kaname -- kaname-mcp
111
+ ```
112
+
113
+ **Claude Desktop** (`claude_desktop_config.json`):
114
+
115
+ ```jsonc
116
+ {
117
+ "mcpServers": {
118
+ "kaname": { "command": "kaname-mcp" }
119
+ }
120
+ }
121
+ ```
122
+
123
+ If `kaname-mcp` isn't on the client's PATH (GUI apps often have a minimal one),
124
+ use an absolute path — `which kaname-mcp` to find it — or point at the built
125
+ file: `"command": "node", "args": ["/abs/path/to/cli/dist/mcp.js"]`.
126
+
127
+ To target a non-default backend, set `KANAME_API_URL` (or persist it via
128
+ `kaname login --api-url …` before connecting).
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/config.ts
4
+ import fs from "fs";
5
+ import os from "os";
6
+ import path from "path";
7
+ function configDir() {
8
+ const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
9
+ return path.join(base, "kaname");
10
+ }
11
+ function cacheDir() {
12
+ const base = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
13
+ return path.join(base, "kaname");
14
+ }
15
+ function readJson(file) {
16
+ try {
17
+ return JSON.parse(fs.readFileSync(file, "utf8"));
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ function writeJson(file, data, mode) {
23
+ fs.mkdirSync(path.dirname(file), { recursive: true });
24
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n", { mode });
25
+ if (mode !== void 0) {
26
+ fs.chmodSync(file, mode);
27
+ }
28
+ }
29
+ var configFile = () => path.join(configDir(), "config.json");
30
+ var credentialsFile = () => path.join(configDir(), "credentials.json");
31
+ var lastViewFile = () => path.join(cacheDir(), "last-view.json");
32
+ function loadConfig() {
33
+ return readJson(configFile()) ?? {};
34
+ }
35
+ function saveConfig(config) {
36
+ writeJson(configFile(), config);
37
+ }
38
+ var DEFAULT_API_URL = "https://kaname-api-p0su.onrender.com";
39
+ function apiUrl() {
40
+ return process.env.KANAME_API_URL || loadConfig().apiUrl || DEFAULT_API_URL;
41
+ }
42
+ function loadCredentials() {
43
+ return readJson(credentialsFile());
44
+ }
45
+ function saveCredentials(creds) {
46
+ writeJson(credentialsFile(), creds, 384);
47
+ }
48
+ function clearCredentials() {
49
+ try {
50
+ fs.unlinkSync(credentialsFile());
51
+ } catch {
52
+ }
53
+ }
54
+ function saveLastView(ids) {
55
+ writeJson(lastViewFile(), { ids });
56
+ }
57
+ function loadLastView() {
58
+ return readJson(lastViewFile())?.ids ?? [];
59
+ }
60
+
61
+ // src/api/client.ts
62
+ var ApiError = class extends Error {
63
+ constructor(message, status) {
64
+ super(message);
65
+ this.status = status;
66
+ }
67
+ status;
68
+ };
69
+ var AuthError = class extends Error {
70
+ };
71
+ var SKIP_REFRESH = ["/auth/login", "/auth/register", "/auth/refresh", "/auth/google", "/auth/apple", "/auth/2fa"];
72
+ var refreshPromise = null;
73
+ async function refreshTokens() {
74
+ if (refreshPromise) return refreshPromise;
75
+ const creds = loadCredentials();
76
+ if (!creds) return false;
77
+ refreshPromise = (async () => {
78
+ try {
79
+ const res = await fetch(`${apiUrl()}/auth/refresh`, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify({ refresh_token: creds.refreshToken })
83
+ });
84
+ if (!res.ok) return false;
85
+ const data = await res.json();
86
+ saveCredentials({ accessToken: data.access_token, refreshToken: data.refresh_token });
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ } finally {
91
+ refreshPromise = null;
92
+ }
93
+ })();
94
+ return refreshPromise;
95
+ }
96
+ async function request(method, endpoint, body, isRetry = false) {
97
+ const headers = { "Content-Type": "application/json" };
98
+ const creds = loadCredentials();
99
+ if (creds) {
100
+ headers["Authorization"] = `Bearer ${creds.accessToken}`;
101
+ } else if (!SKIP_REFRESH.some((e) => endpoint.startsWith(e))) {
102
+ throw new AuthError("Not logged in. Run `kaname login`.");
103
+ }
104
+ let res;
105
+ try {
106
+ res = await fetch(`${apiUrl()}${endpoint}`, {
107
+ method,
108
+ headers,
109
+ body: body !== void 0 ? JSON.stringify(body) : void 0
110
+ });
111
+ } catch (err) {
112
+ throw new ApiError(`Could not reach ${apiUrl()} (${err.message})`, 0);
113
+ }
114
+ if (res.status === 204) return void 0;
115
+ const shouldSkipRefresh = SKIP_REFRESH.some((e) => endpoint.startsWith(e));
116
+ if (res.status === 401 && !isRetry && !shouldSkipRefresh) {
117
+ if (await refreshTokens()) {
118
+ return request(method, endpoint, body, true);
119
+ }
120
+ clearCredentials();
121
+ throw new AuthError("Session expired. Run `kaname login`.");
122
+ }
123
+ if (!res.ok) {
124
+ let detail = `HTTP ${res.status}`;
125
+ try {
126
+ const data = await res.json();
127
+ if (typeof data.detail === "string") detail = data.detail;
128
+ else if (data.detail) detail = JSON.stringify(data.detail);
129
+ } catch {
130
+ }
131
+ throw new ApiError(detail, res.status);
132
+ }
133
+ return await res.json();
134
+ }
135
+ var api = {
136
+ get: (endpoint) => request("GET", endpoint),
137
+ post: (endpoint, body) => request("POST", endpoint, body),
138
+ put: (endpoint, body) => request("PUT", endpoint, body),
139
+ del: (endpoint) => request("DELETE", endpoint)
140
+ };
141
+
142
+ // src/util/dates.ts
143
+ var WEEKDAYS = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"];
144
+ function toIso(d) {
145
+ const y = d.getFullYear();
146
+ const m = String(d.getMonth() + 1).padStart(2, "0");
147
+ const day = String(d.getDate()).padStart(2, "0");
148
+ return `${y}-${m}-${day}`;
149
+ }
150
+ function localToday(now = /* @__PURE__ */ new Date()) {
151
+ return toIso(now);
152
+ }
153
+ function parseDateArg(input, now = /* @__PURE__ */ new Date()) {
154
+ const s = input.trim().toLowerCase();
155
+ if (s === "none" || s === "clear") return null;
156
+ if (s === "today" || s === "tod") return toIso(now);
157
+ if (s === "tomorrow" || s === "tom") {
158
+ const d = new Date(now);
159
+ d.setDate(d.getDate() + 1);
160
+ return toIso(d);
161
+ }
162
+ const offset = s.match(/^\+?(\d{1,3})d$/);
163
+ if (offset) {
164
+ const d = new Date(now);
165
+ d.setDate(d.getDate() + Number(offset[1]));
166
+ return toIso(d);
167
+ }
168
+ const weekdayIndex = WEEKDAYS.findIndex((w) => w === s || w.slice(0, 3) === s);
169
+ if (weekdayIndex >= 0) {
170
+ const d = new Date(now);
171
+ d.setDate(d.getDate() + (weekdayIndex - d.getDay() + 7) % 7);
172
+ return toIso(d);
173
+ }
174
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s)) {
175
+ const parsed = /* @__PURE__ */ new Date(`${s}T00:00:00`);
176
+ if (Number.isNaN(parsed.getTime())) throw new Error(`Invalid date: ${input}`);
177
+ return s;
178
+ }
179
+ throw new Error(`Cannot parse date "${input}" (try: today, tomorrow, fri, +3d, 2026-07-10)`);
180
+ }
181
+
182
+ // src/commands/lists.ts
183
+ import pc2 from "picocolors";
184
+
185
+ // src/util/confirm.ts
186
+ import * as p from "@clack/prompts";
187
+ async function confirmOrFail(message) {
188
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
189
+ throw new Error("Refusing to prompt in non-interactive mode \u2014 pass --force to skip confirmation.");
190
+ }
191
+ const ok = await p.confirm({ message, initialValue: false });
192
+ return !p.isCancel(ok) && ok === true;
193
+ }
194
+
195
+ // src/util/output.ts
196
+ import pc from "picocolors";
197
+ function printJson(data) {
198
+ console.log(JSON.stringify(data, null, 2));
199
+ }
200
+ function dateLabel(iso, time) {
201
+ if (!iso) return "";
202
+ const today = localToday();
203
+ let label = iso;
204
+ if (iso === today) label = "today";
205
+ else {
206
+ const tomorrow = /* @__PURE__ */ new Date();
207
+ tomorrow.setDate(tomorrow.getDate() + 1);
208
+ if (iso === localToday(tomorrow)) label = "tomorrow";
209
+ }
210
+ if (time) label += ` ${time.slice(0, 5)}`;
211
+ return label;
212
+ }
213
+ function colorDate(label, iso, completed) {
214
+ if (!label) return "";
215
+ if (!completed && iso && iso < localToday()) return pc.red(label);
216
+ if (label.startsWith("today")) return pc.yellow(label);
217
+ return pc.cyan(label);
218
+ }
219
+ function printTodos(todos, opts, listNames) {
220
+ const ids = todos.map((t) => t.id);
221
+ if (opts.json) {
222
+ printJson(todos);
223
+ return ids;
224
+ }
225
+ if (opts.plain) {
226
+ for (const t of todos) {
227
+ console.log([t.id, t.completed ? "done" : "open", t.title, t.when_date ?? "", t.due_date ?? ""].join(" "));
228
+ }
229
+ return ids;
230
+ }
231
+ if (todos.length === 0) {
232
+ console.log(pc.dim("No todos."));
233
+ return ids;
234
+ }
235
+ const indexWidth = String(todos.length).length + 1;
236
+ todos.forEach((t, i) => {
237
+ const index = pc.dim(`#${i + 1}`.padStart(indexWidth + 1));
238
+ const mark = t.completed ? pc.green("\u2713") : "\u25CB";
239
+ const title = t.completed ? pc.dim(t.title) : t.title;
240
+ const parts = [index, mark, title];
241
+ const when = colorDate(dateLabel(t.when_date, t.when_time), t.when_date, t.completed);
242
+ if (when) parts.push(when);
243
+ if (t.due_date) {
244
+ parts.push(pc.dim("due ") + colorDate(dateLabel(t.due_date, t.due_time), t.due_date, t.completed));
245
+ }
246
+ if (t.list_id && listNames?.has(t.list_id)) {
247
+ parts.push(pc.magenta(`[${listNames.get(t.list_id)}]`));
248
+ }
249
+ if (t.recurrence_rule) parts.push(pc.dim("\u21BB"));
250
+ console.log(parts.join(" "));
251
+ });
252
+ return ids;
253
+ }
254
+ function printTodoDetail(todo, opts) {
255
+ if (opts.json) {
256
+ printJson(todo);
257
+ return;
258
+ }
259
+ console.log(`${todo.completed ? pc.green("\u2713") : "\u25CB"} ${pc.bold(todo.title)}`);
260
+ console.log(pc.dim(` id: ${todo.id}`));
261
+ if (todo.description) console.log(` ${todo.description}`);
262
+ if (todo.when_date) console.log(` when: ${colorDate(dateLabel(todo.when_date, todo.when_time), todo.when_date, todo.completed)}`);
263
+ if (todo.due_date) console.log(` due: ${colorDate(dateLabel(todo.due_date, todo.due_time), todo.due_date, todo.completed)}`);
264
+ if (todo.recurrence_rule) console.log(pc.dim(` repeats: ${todo.recurrence_rule}`));
265
+ const subtasks = todo.subtasks ?? [];
266
+ if (subtasks.length > 0) {
267
+ console.log(" subtasks:");
268
+ subtasks.forEach((s, i) => {
269
+ console.log(` ${pc.dim(String(i + 1) + ".")} ${s.completed ? pc.green("\u2713") : "\u25CB"} ${s.completed ? pc.dim(s.title) : s.title}`);
270
+ });
271
+ }
272
+ }
273
+
274
+ // src/commands/lists.ts
275
+ function flattenLists(lists) {
276
+ const flat = [];
277
+ const walk = (nodes) => {
278
+ for (const node of nodes) {
279
+ flat.push(node);
280
+ if (node.children?.length) walk(node.children);
281
+ }
282
+ };
283
+ walk(lists);
284
+ return flat;
285
+ }
286
+ async function fetchLists() {
287
+ return api.get("/lists");
288
+ }
289
+ function resolveList(lists, name) {
290
+ const flat = flattenLists(lists);
291
+ const lower = name.toLowerCase();
292
+ const exact = flat.filter((l) => l.name.toLowerCase() === lower);
293
+ if (exact.length === 1) return exact[0];
294
+ if (exact.length > 1) throw new Error(`Multiple lists named "${name}" \u2014 use the list id.`);
295
+ const prefix = flat.filter((l) => l.name.toLowerCase().startsWith(lower));
296
+ if (prefix.length === 1) return prefix[0];
297
+ if (prefix.length > 1) {
298
+ throw new Error(`Ambiguous list "${name}": ${prefix.map((l) => l.name).join(", ")}`);
299
+ }
300
+ throw new Error(`No list named "${name}". Run \`kaname list ls\`.`);
301
+ }
302
+ async function listNameMap() {
303
+ try {
304
+ return new Map(flattenLists(await fetchLists()).map((l) => [l.id, l.name]));
305
+ } catch {
306
+ return /* @__PURE__ */ new Map();
307
+ }
308
+ }
309
+ function printTree(lists, opts) {
310
+ if (opts.json) {
311
+ printJson(lists);
312
+ return;
313
+ }
314
+ if (lists.length === 0) {
315
+ console.log(pc2.dim("No lists."));
316
+ return;
317
+ }
318
+ const walk = (nodes, depth) => {
319
+ for (const node of nodes) {
320
+ const indent = " ".repeat(depth);
321
+ const count = pc2.dim(`(${node.todo_count})`);
322
+ const shared = node.is_shared ? pc2.blue(node.is_owner ? " [shared]" : ` [via ${node.owner_email}]`) : "";
323
+ console.log(`${indent}${node.name} ${count}${shared}`);
324
+ if (node.children?.length) walk(node.children, depth + 1);
325
+ }
326
+ };
327
+ walk(lists, 0);
328
+ }
329
+ function registerListCommands(program) {
330
+ const list = program.command("list").description("Manage lists");
331
+ list.command("ls").description("Show all lists").action(async () => {
332
+ printTree(await fetchLists(), program.opts());
333
+ });
334
+ list.command("add <name>").description("Create a list").option("--parent <list>", "parent list name").option("--color <color>", "list color").action(async (name, opts) => {
335
+ let parentId;
336
+ if (opts.parent) parentId = resolveList(await fetchLists(), opts.parent).id;
337
+ const created = await api.post("/lists", {
338
+ name,
339
+ color: opts.color,
340
+ parent_id: parentId
341
+ });
342
+ console.log(pc2.green(`Created list "${created.name}"`));
343
+ });
344
+ list.command("rename <list> <newName>").description("Rename a list").action(async (listName, newName) => {
345
+ const target = resolveList(await fetchLists(), listName);
346
+ await api.put(`/lists/${target.id}`, { name: newName });
347
+ console.log(`Renamed "${target.name}" \u2192 "${newName}"`);
348
+ });
349
+ list.command("rm <list>").description("Delete a list (and its todos)").option("-f, --force", "skip confirmation").action(async (listName, opts) => {
350
+ const target = resolveList(await fetchLists(), listName);
351
+ if (!opts.force && !await confirmOrFail(`Delete list "${target.name}" (${target.todo_count} todos)?`)) {
352
+ return;
353
+ }
354
+ await api.del(`/lists/${target.id}`);
355
+ console.log(`Deleted list "${target.name}"`);
356
+ });
357
+ }
358
+
359
+ export {
360
+ loadConfig,
361
+ saveConfig,
362
+ apiUrl,
363
+ loadCredentials,
364
+ saveCredentials,
365
+ clearCredentials,
366
+ saveLastView,
367
+ loadLastView,
368
+ ApiError,
369
+ AuthError,
370
+ api,
371
+ confirmOrFail,
372
+ localToday,
373
+ parseDateArg,
374
+ printJson,
375
+ printTodos,
376
+ printTodoDetail,
377
+ flattenLists,
378
+ fetchLists,
379
+ resolveList,
380
+ listNameMap,
381
+ registerListCommands
382
+ };
package/dist/index.js ADDED
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ApiError,
4
+ AuthError,
5
+ api,
6
+ apiUrl,
7
+ clearCredentials,
8
+ confirmOrFail,
9
+ fetchLists,
10
+ listNameMap,
11
+ loadConfig,
12
+ loadLastView,
13
+ localToday,
14
+ parseDateArg,
15
+ printJson,
16
+ printTodoDetail,
17
+ printTodos,
18
+ registerListCommands,
19
+ resolveList,
20
+ saveConfig,
21
+ saveCredentials,
22
+ saveLastView
23
+ } from "./chunk-T7KF3F3M.js";
24
+
25
+ // src/index.ts
26
+ import { Command } from "commander";
27
+ import pc3 from "picocolors";
28
+
29
+ // src/commands/auth.ts
30
+ import crypto from "crypto";
31
+ import http from "http";
32
+ import { spawn } from "child_process";
33
+ import * as p from "@clack/prompts";
34
+ import pc from "picocolors";
35
+ function saveTokenPair(token) {
36
+ saveCredentials({ accessToken: token.access_token, refreshToken: token.refresh_token });
37
+ }
38
+ function openBrowser(url) {
39
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
40
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
41
+ try {
42
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
43
+ } catch {
44
+ }
45
+ }
46
+ function googleOAuthConfig() {
47
+ const config = loadConfig();
48
+ const clientId = process.env.KANAME_GOOGLE_CLIENT_ID || config.googleClientId;
49
+ const clientSecret = process.env.KANAME_GOOGLE_CLIENT_SECRET || config.googleClientSecret;
50
+ if (!clientId || !clientSecret) {
51
+ throw new Error(
52
+ 'Google OAuth is not configured. Set KANAME_GOOGLE_CLIENT_ID / KANAME_GOOGLE_CLIENT_SECRET (a "Desktop app" OAuth client), or use `kaname login --password`.'
53
+ );
54
+ }
55
+ return { clientId, clientSecret };
56
+ }
57
+ async function googleIdToken({ clientId, clientSecret }) {
58
+ const verifier = crypto.randomBytes(32).toString("base64url");
59
+ const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
60
+ const state = crypto.randomBytes(16).toString("base64url");
61
+ let redirectUri = "";
62
+ const code = await new Promise((resolve, reject) => {
63
+ const server = http.createServer((req, res2) => {
64
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
65
+ if (url.pathname !== "/callback") {
66
+ res2.writeHead(404).end();
67
+ return;
68
+ }
69
+ res2.writeHead(200, { "Content-Type": "text/html" });
70
+ res2.end("<html><body><p>You are logged in to the Kaname CLI. You can close this tab.</p></body></html>");
71
+ server.close();
72
+ const err = url.searchParams.get("error");
73
+ if (err) return reject(new Error(`Google login failed: ${err}`));
74
+ if (url.searchParams.get("state") !== state) return reject(new Error("OAuth state mismatch."));
75
+ const c = url.searchParams.get("code");
76
+ if (!c) return reject(new Error("No authorization code in callback."));
77
+ resolve(c);
78
+ });
79
+ server.on("error", reject);
80
+ server.listen(0, "127.0.0.1", () => {
81
+ const address = server.address();
82
+ if (address === null || typeof address === "string") return reject(new Error("Could not bind loopback port."));
83
+ redirectUri = `http://127.0.0.1:${address.port}/callback`;
84
+ const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
85
+ authUrl.search = new URLSearchParams({
86
+ client_id: clientId,
87
+ redirect_uri: redirectUri,
88
+ response_type: "code",
89
+ scope: "openid email",
90
+ code_challenge: challenge,
91
+ code_challenge_method: "S256",
92
+ state
93
+ }).toString();
94
+ console.log("Opening your browser for Google sign-in\u2026");
95
+ console.log(pc.dim(`If it doesn't open, visit:
96
+ ${authUrl}`));
97
+ openBrowser(authUrl.toString());
98
+ });
99
+ });
100
+ const res = await fetch("https://oauth2.googleapis.com/token", {
101
+ method: "POST",
102
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
103
+ body: new URLSearchParams({
104
+ code,
105
+ client_id: clientId,
106
+ client_secret: clientSecret,
107
+ redirect_uri: redirectUri,
108
+ grant_type: "authorization_code",
109
+ code_verifier: verifier
110
+ })
111
+ });
112
+ const data = await res.json();
113
+ if (!res.ok || !data.id_token) {
114
+ throw new Error(`Google token exchange failed: ${data.error_description || data.error || res.status}`);
115
+ }
116
+ return data.id_token;
117
+ }
118
+ async function passwordLogin() {
119
+ const email = await p.text({ message: "Email", validate: (v) => v.includes("@") ? void 0 : "Enter an email" });
120
+ if (p.isCancel(email)) process.exit(130);
121
+ const password2 = await p.password({ message: "Password" });
122
+ if (p.isCancel(password2)) process.exit(130);
123
+ const result = await api.post("/auth/login", {
124
+ email,
125
+ password: password2
126
+ });
127
+ if ("pending_2fa_token" in result) {
128
+ await twoFactorLogin(result.pending_2fa_token);
129
+ return;
130
+ }
131
+ saveTokenPair(result);
132
+ }
133
+ async function twoFactorLogin(pendingToken) {
134
+ const authHeaders = { Authorization: `Bearer ${pendingToken}`, "Content-Type": "application/json" };
135
+ const sendRes = await fetch(`${apiUrl()}/auth/2fa/send-otp`, { method: "POST", headers: authHeaders });
136
+ if (!sendRes.ok) throw new ApiError("Failed to send verification code.", sendRes.status);
137
+ console.log("A verification code was sent to your email.");
138
+ const code = await p.text({ message: "Verification code" });
139
+ if (p.isCancel(code)) process.exit(130);
140
+ const verifyRes = await fetch(`${apiUrl()}/auth/2fa/verify-otp`, {
141
+ method: "POST",
142
+ headers: authHeaders,
143
+ body: JSON.stringify({ code })
144
+ });
145
+ if (!verifyRes.ok) throw new ApiError("Invalid verification code.", verifyRes.status);
146
+ const token = await verifyRes.json();
147
+ saveTokenPair(token);
148
+ }
149
+ function registerAuthCommands(program2) {
150
+ program2.command("login").description("Log in with Google (default) or email/password").option("--password", "log in with email and password instead of Google").option("--api-url <url>", "backend URL to use (persisted to config)").action(async (opts) => {
151
+ if (opts.apiUrl) {
152
+ saveConfig({ ...loadConfig(), apiUrl: opts.apiUrl });
153
+ console.log(`API URL set to ${opts.apiUrl}`);
154
+ }
155
+ if (opts.password) {
156
+ await passwordLogin();
157
+ } else {
158
+ const idToken = await googleIdToken(googleOAuthConfig());
159
+ const token = await api.post("/auth/google", { credential: idToken });
160
+ saveTokenPair(token);
161
+ }
162
+ const me = await api.get("/auth/me");
163
+ console.log(pc.green(`Logged in as ${me.email}`));
164
+ });
165
+ program2.command("logout").description("Remove stored credentials").action(() => {
166
+ clearCredentials();
167
+ console.log("Logged out.");
168
+ });
169
+ program2.command("whoami").description("Show the logged-in account").action(async () => {
170
+ try {
171
+ const me = await api.get("/auth/me");
172
+ console.log(`${me.email} ${pc.dim(`(${me.id}, ${apiUrl()})`)}`);
173
+ } catch (err) {
174
+ if (err instanceof AuthError) {
175
+ console.log(`Not logged in ${pc.dim(`(${apiUrl()})`)}`);
176
+ process.exitCode = 2;
177
+ return;
178
+ }
179
+ throw err;
180
+ }
181
+ });
182
+ }
183
+
184
+ // src/commands/todos.ts
185
+ import pc2 from "picocolors";
186
+
187
+ // src/util/ids.ts
188
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
189
+ function resolveTodoId(token) {
190
+ const indexMatch = token.match(/^#?(\d{1,4})$/);
191
+ if (indexMatch) {
192
+ const ids = loadLastView();
193
+ const index = Number(indexMatch[1]);
194
+ if (index < 1 || index > ids.length) {
195
+ throw new Error(
196
+ ids.length === 0 ? `No cached listing \u2014 run \`kaname ls\` first, or pass a UUID.` : `#${index} is out of range (last listing had ${ids.length} items).`
197
+ );
198
+ }
199
+ return ids[index - 1];
200
+ }
201
+ if (UUID_RE.test(token)) return token;
202
+ if (/^[0-9a-f]{4,}$/i.test(token)) {
203
+ const matches = loadLastView().filter((id) => id.toLowerCase().startsWith(token.toLowerCase()));
204
+ if (matches.length === 1) return matches[0];
205
+ if (matches.length > 1) throw new Error(`Ambiguous id prefix "${token}" (${matches.length} matches).`);
206
+ throw new Error(`No todo in the last listing matches "${token}" \u2014 run \`kaname ls\` or pass a full UUID.`);
207
+ }
208
+ throw new Error(`"${token}" is not a #index, UUID, or UUID prefix.`);
209
+ }
210
+
211
+ // src/commands/todos.ts
212
+ async function buildEditPayload(opts) {
213
+ const payload = {};
214
+ if (opts.title !== void 0) payload.title = opts.title;
215
+ if (opts.notes !== void 0) payload.description = opts.notes;
216
+ if (opts.when !== void 0) payload.when_date = parseDateArg(opts.when);
217
+ if (opts.due !== void 0) payload.due_date = parseDateArg(opts.due);
218
+ if (opts.list !== void 0) {
219
+ payload.list_id = opts.list === "none" ? null : resolveList(await fetchLists(), opts.list).id;
220
+ }
221
+ return payload;
222
+ }
223
+ function registerTodoCommands(program2) {
224
+ program2.command("add <title...>").description("Add a todo").option("-l, --list <list>", "list name").option("-w, --when <date>", "when to do it (today, tomorrow, fri, +3d, YYYY-MM-DD)").option("-d, --due <date>", "deadline").option("-n, --notes <text>", "description").action(async (titleWords, opts) => {
225
+ const payload = { title: titleWords.join(" ") };
226
+ if (opts.notes) payload.description = opts.notes;
227
+ if (opts.when) payload.when_date = parseDateArg(opts.when);
228
+ if (opts.due) payload.due_date = parseDateArg(opts.due);
229
+ if (opts.list) payload.list_id = resolveList(await fetchLists(), opts.list).id;
230
+ const todo = await api.post("/todos", payload);
231
+ const globalOpts = program2.opts();
232
+ if (globalOpts.json) printJson(todo);
233
+ else console.log(pc2.green("Added:") + ` ${todo.title} ${pc2.dim(todo.id)}`);
234
+ });
235
+ program2.command("ls").description("List todos (default: today view)").option("-t, --today", "todos scheduled for today (default)").option("-i, --inbox", "todos with no list").option("-a, --all", "all todos").option("-l, --list <list>", "todos in a list").option("-s, --smart <name>", "todos in a smart list").option("--status <status>", "filter by completion: open, done, or all", "all").action(
236
+ async (opts) => {
237
+ const status = (opts.status ?? "all").toLowerCase();
238
+ if (!["open", "done", "all"].includes(status)) {
239
+ throw new Error(`--status must be one of: open, done, all (got "${opts.status}").`);
240
+ }
241
+ const params = new URLSearchParams();
242
+ let annotateLists = true;
243
+ if (opts.all) {
244
+ params.set("all", "true");
245
+ } else if (opts.inbox) {
246
+ params.set("inbox", "true");
247
+ annotateLists = false;
248
+ } else if (opts.list) {
249
+ const target = resolveList(await fetchLists(), opts.list);
250
+ params.set("list_id", target.id);
251
+ annotateLists = false;
252
+ } else if (opts.smart) {
253
+ const smartLists = await api.get("/smart-lists");
254
+ const lower = opts.smart.toLowerCase();
255
+ const match = smartLists.filter((s) => s.name.toLowerCase().startsWith(lower));
256
+ if (match.length !== 1) {
257
+ throw new Error(
258
+ match.length === 0 ? `No smart list matching "${opts.smart}".` : `Ambiguous smart list "${opts.smart}": ${match.map((s) => s.name).join(", ")}`
259
+ );
260
+ }
261
+ params.set("smart_list_id", match[0].id);
262
+ } else {
263
+ params.set("today", "true");
264
+ }
265
+ params.set("client_date", localToday());
266
+ const todos = await api.get(`/todos?${params}`);
267
+ const filtered = status === "all" ? todos : todos.filter((t) => t.completed === (status === "done"));
268
+ const names = annotateLists ? await listNameMap() : void 0;
269
+ const ids = printTodos(filtered, program2.opts(), names);
270
+ saveLastView(ids);
271
+ }
272
+ );
273
+ program2.command("show <id>").description("Show a todo with its subtasks").action(async (idToken) => {
274
+ const todo = await api.get(`/todos/${resolveTodoId(idToken)}`);
275
+ printTodoDetail(todo, program2.opts());
276
+ });
277
+ program2.command("done <ids...>").description("Complete one or more todos").action(async (idTokens) => {
278
+ for (const token of idTokens) {
279
+ const todo = await api.put(`/todos/${resolveTodoId(token)}`, { completed: true });
280
+ console.log(`${pc2.green("\u2713")} ${todo.title}`);
281
+ }
282
+ });
283
+ program2.command("undone <ids...>").description("Reopen one or more completed todos").action(async (idTokens) => {
284
+ for (const token of idTokens) {
285
+ const todo = await api.put(`/todos/${resolveTodoId(token)}`, { completed: false });
286
+ console.log(`\u25CB ${todo.title}`);
287
+ }
288
+ });
289
+ program2.command("edit <id>").description("Edit a todo (only the fields you pass change)").option("--title <title>").option("-n, --notes <text>").option("-w, --when <date>", 'date, or "none" to clear').option("-d, --due <date>", 'date, or "none" to clear').option("-l, --list <list>", 'list name, or "none" for inbox').action(async (idToken, opts) => {
290
+ const payload = await buildEditPayload(opts);
291
+ if (Object.keys(payload).length === 0) {
292
+ throw new Error("Nothing to change \u2014 pass at least one of --title/--notes/--when/--due/--list.");
293
+ }
294
+ const todo = await api.put(`/todos/${resolveTodoId(idToken)}`, payload);
295
+ printTodoDetail(todo, program2.opts());
296
+ });
297
+ program2.command("rm <id>").description("Delete a todo").option("-f, --force", "skip confirmation").action(async (idToken, opts) => {
298
+ const id = resolveTodoId(idToken);
299
+ const todo = await api.get(`/todos/${id}`);
300
+ if (!opts.force && !await confirmOrFail(`Delete "${todo.title}"?`)) return;
301
+ await api.del(`/todos/${id}`);
302
+ console.log(`Deleted "${todo.title}"`);
303
+ });
304
+ const subtask = program2.command("subtask").description("Manage subtasks of a todo");
305
+ subtask.command("add <id> <title...>").description("Add a subtask").action(async (idToken, titleWords) => {
306
+ const created = await api.post(
307
+ `/todos/${resolveTodoId(idToken)}/subtasks`,
308
+ { title: titleWords.join(" ") }
309
+ );
310
+ console.log(pc2.green("Added subtask:") + ` ${created.title}`);
311
+ });
312
+ const resolveSubtask = async (idToken, indexArg) => {
313
+ const todoId = resolveTodoId(idToken);
314
+ const todo = await api.get(`/todos/${todoId}`);
315
+ const subtasks = todo.subtasks ?? [];
316
+ const index = Number(indexArg);
317
+ if (!Number.isInteger(index) || index < 1 || index > subtasks.length) {
318
+ throw new Error(`Subtask index must be 1\u2013${subtasks.length} (see \`kaname show ${idToken}\`).`);
319
+ }
320
+ return { todoId, subtask: subtasks[index - 1] };
321
+ };
322
+ subtask.command("done <id> <n>").description("Complete subtask #n of a todo").action(async (idToken, n) => {
323
+ const { todoId, subtask: target } = await resolveSubtask(idToken, n);
324
+ await api.put(`/todos/${todoId}/subtasks/${target.id}`, { completed: true });
325
+ console.log(`${pc2.green("\u2713")} ${target.title}`);
326
+ });
327
+ subtask.command("rm <id> <n>").description("Delete subtask #n of a todo").action(async (idToken, n) => {
328
+ const { todoId, subtask: target } = await resolveSubtask(idToken, n);
329
+ await api.del(`/todos/${todoId}/subtasks/${target.id}`);
330
+ console.log(`Deleted subtask "${target.title}"`);
331
+ });
332
+ }
333
+
334
+ // src/index.ts
335
+ var program = new Command();
336
+ program.name("kaname").description("Kaname from the command line").version("0.1.0").option("-j, --json", "output raw JSON (for scripting)").option("--plain", "plain tab-separated output (no color)");
337
+ registerAuthCommands(program);
338
+ registerTodoCommands(program);
339
+ registerListCommands(program);
340
+ program.parseAsync().catch((err) => {
341
+ if (err instanceof AuthError) {
342
+ console.error(pc3.red(err.message));
343
+ process.exit(2);
344
+ }
345
+ if (err instanceof ApiError || err instanceof Error) {
346
+ console.error(pc3.red(err.message));
347
+ process.exit(1);
348
+ }
349
+ console.error(pc3.red(String(err)));
350
+ process.exit(1);
351
+ });
package/dist/mcp.js ADDED
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ApiError,
4
+ AuthError,
5
+ api,
6
+ fetchLists,
7
+ flattenLists,
8
+ loadCredentials,
9
+ localToday,
10
+ parseDateArg,
11
+ resolveList
12
+ } from "./chunk-T7KF3F3M.js";
13
+
14
+ // src/mcp.ts
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
17
+ import { z } from "zod";
18
+ function ok(data) {
19
+ const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
20
+ return { content: [{ type: "text", text }] };
21
+ }
22
+ async function run(fn) {
23
+ try {
24
+ return ok(await fn());
25
+ } catch (err) {
26
+ if (err instanceof AuthError) {
27
+ return { content: [{ type: "text", text: "Not logged in. Run `kaname login` in a terminal." }], isError: true };
28
+ }
29
+ const msg = err instanceof ApiError ? `${err.message} (HTTP ${err.status})` : err.message;
30
+ return { content: [{ type: "text", text: msg }], isError: true };
31
+ }
32
+ }
33
+ async function resolveListArg(nameOrId) {
34
+ const lists = await fetchLists();
35
+ const byId = flattenLists(lists).find((l) => l.id === nameOrId);
36
+ return byId ?? resolveList(lists, nameOrId);
37
+ }
38
+ var dateArg = z.string().describe('today, tomorrow, a weekday (fri), a relative offset (+3d), an ISO date (2026-07-10), or "none" to clear');
39
+ function buildServer() {
40
+ const server = new McpServer({ name: "kaname", version: "0.1.0" });
41
+ server.registerTool(
42
+ "list_todos",
43
+ {
44
+ title: "List todos",
45
+ description: 'List todos. Default view is "today". Use view "list" with `list`, or "smart" with `smart`. Filter by completion with `status`. Returns todo items including their ids for use with other tools.',
46
+ inputSchema: {
47
+ view: z.enum(["today", "inbox", "all", "list", "smart"]).default("today"),
48
+ list: z.string().optional().describe('list name or id (required when view="list")'),
49
+ smart: z.string().optional().describe('smart list name (required when view="smart")'),
50
+ status: z.enum(["open", "done", "all"]).default("all")
51
+ }
52
+ },
53
+ async ({ view, list, smart, status }) => run(async () => {
54
+ const params = new URLSearchParams();
55
+ if (view === "all") params.set("all", "true");
56
+ else if (view === "inbox") params.set("inbox", "true");
57
+ else if (view === "list") {
58
+ if (!list) throw new Error('view="list" requires `list`.');
59
+ params.set("list_id", (await resolveListArg(list)).id);
60
+ } else if (view === "smart") {
61
+ if (!smart) throw new Error('view="smart" requires `smart`.');
62
+ const smartLists = await api.get("/smart-lists");
63
+ const lower = smart.toLowerCase();
64
+ const match = smartLists.filter((s) => s.name.toLowerCase().startsWith(lower));
65
+ if (match.length !== 1) {
66
+ throw new Error(
67
+ match.length === 0 ? `No smart list matching "${smart}".` : `Ambiguous smart list "${smart}": ${match.map((s) => s.name).join(", ")}`
68
+ );
69
+ }
70
+ params.set("smart_list_id", match[0].id);
71
+ } else {
72
+ params.set("today", "true");
73
+ }
74
+ params.set("client_date", localToday());
75
+ const todos = await api.get(`/todos?${params}`);
76
+ return status === "all" ? todos : todos.filter((t) => t.completed === (status === "done"));
77
+ })
78
+ );
79
+ server.registerTool(
80
+ "get_todo",
81
+ {
82
+ title: "Get todo",
83
+ description: "Get a single todo by id, including its subtasks.",
84
+ inputSchema: { id: z.string().describe("todo id (uuid)") }
85
+ },
86
+ async ({ id }) => run(() => api.get(`/todos/${id}`))
87
+ );
88
+ server.registerTool(
89
+ "add_todo",
90
+ {
91
+ title: "Add todo",
92
+ description: "Create a new todo.",
93
+ inputSchema: {
94
+ title: z.string(),
95
+ notes: z.string().optional().describe("description / notes"),
96
+ when: dateArg.optional().describe("when to do it"),
97
+ due: dateArg.optional().describe("deadline"),
98
+ list: z.string().optional().describe("list name or id")
99
+ }
100
+ },
101
+ async ({ title, notes, when, due, list }) => run(async () => {
102
+ const payload = { title };
103
+ if (notes !== void 0) payload.description = notes;
104
+ if (when !== void 0) payload.when_date = parseDateArg(when);
105
+ if (due !== void 0) payload.due_date = parseDateArg(due);
106
+ if (list !== void 0) payload.list_id = (await resolveListArg(list)).id;
107
+ return api.post("/todos", payload);
108
+ })
109
+ );
110
+ server.registerTool(
111
+ "update_todo",
112
+ {
113
+ title: "Update todo",
114
+ description: 'Update fields of a todo. Only the fields you pass change. Pass "none" to when/due/list to clear them.',
115
+ inputSchema: {
116
+ id: z.string().describe("todo id (uuid)"),
117
+ title: z.string().optional(),
118
+ notes: z.string().optional(),
119
+ when: dateArg.optional(),
120
+ due: dateArg.optional(),
121
+ list: z.string().optional().describe('list name or id, or "none" for inbox')
122
+ }
123
+ },
124
+ async ({ id, title, notes, when, due, list }) => run(async () => {
125
+ const payload = {};
126
+ if (title !== void 0) payload.title = title;
127
+ if (notes !== void 0) payload.description = notes;
128
+ if (when !== void 0) payload.when_date = parseDateArg(when);
129
+ if (due !== void 0) payload.due_date = parseDateArg(due);
130
+ if (list !== void 0) payload.list_id = list.toLowerCase() === "none" ? null : (await resolveListArg(list)).id;
131
+ if (Object.keys(payload).length === 0) {
132
+ throw new Error("Nothing to change \u2014 pass at least one of title/notes/when/due/list.");
133
+ }
134
+ return api.put(`/todos/${id}`, payload);
135
+ })
136
+ );
137
+ server.registerTool(
138
+ "complete_todo",
139
+ {
140
+ title: "Complete todo",
141
+ description: "Mark a todo as done.",
142
+ inputSchema: { id: z.string().describe("todo id (uuid)") }
143
+ },
144
+ async ({ id }) => run(() => api.put(`/todos/${id}`, { completed: true }))
145
+ );
146
+ server.registerTool(
147
+ "reopen_todo",
148
+ {
149
+ title: "Reopen todo",
150
+ description: "Mark a completed todo as open again.",
151
+ inputSchema: { id: z.string().describe("todo id (uuid)") }
152
+ },
153
+ async ({ id }) => run(() => api.put(`/todos/${id}`, { completed: false }))
154
+ );
155
+ server.registerTool(
156
+ "delete_todo",
157
+ {
158
+ title: "Delete todo",
159
+ description: "Permanently delete a todo and its subtasks.",
160
+ inputSchema: { id: z.string().describe("todo id (uuid)") }
161
+ },
162
+ async ({ id }) => run(async () => {
163
+ await api.del(`/todos/${id}`);
164
+ return `Deleted todo ${id}.`;
165
+ })
166
+ );
167
+ server.registerTool(
168
+ "add_subtask",
169
+ {
170
+ title: "Add subtask",
171
+ description: "Add a subtask to a todo.",
172
+ inputSchema: { todo_id: z.string().describe("parent todo id (uuid)"), title: z.string() }
173
+ },
174
+ async ({ todo_id, title }) => run(() => api.post(`/todos/${todo_id}/subtasks`, { title }))
175
+ );
176
+ server.registerTool(
177
+ "update_subtask",
178
+ {
179
+ title: "Update subtask",
180
+ description: "Update a subtask (rename and/or set completion). Get subtask ids from get_todo.",
181
+ inputSchema: {
182
+ todo_id: z.string().describe("parent todo id (uuid)"),
183
+ subtask_id: z.string().describe("subtask id (uuid)"),
184
+ title: z.string().optional(),
185
+ completed: z.boolean().optional()
186
+ }
187
+ },
188
+ async ({ todo_id, subtask_id, title, completed }) => run(() => {
189
+ const payload = {};
190
+ if (title !== void 0) payload.title = title;
191
+ if (completed !== void 0) payload.completed = completed;
192
+ if (Object.keys(payload).length === 0) throw new Error("Pass title and/or completed.");
193
+ return api.put(`/todos/${todo_id}/subtasks/${subtask_id}`, payload);
194
+ })
195
+ );
196
+ server.registerTool(
197
+ "delete_subtask",
198
+ {
199
+ title: "Delete subtask",
200
+ description: "Delete a subtask from a todo.",
201
+ inputSchema: {
202
+ todo_id: z.string().describe("parent todo id (uuid)"),
203
+ subtask_id: z.string().describe("subtask id (uuid)")
204
+ }
205
+ },
206
+ async ({ todo_id, subtask_id }) => run(async () => {
207
+ await api.del(`/todos/${todo_id}/subtasks/${subtask_id}`);
208
+ return `Deleted subtask ${subtask_id}.`;
209
+ })
210
+ );
211
+ server.registerTool(
212
+ "list_lists",
213
+ {
214
+ title: "List lists",
215
+ description: "Show all lists as a tree, with todo counts.",
216
+ inputSchema: {}
217
+ },
218
+ async () => run(() => fetchLists())
219
+ );
220
+ server.registerTool(
221
+ "list_smart_lists",
222
+ {
223
+ title: "List smart lists",
224
+ description: 'Show all smart lists (saved filters) usable with list_todos view="smart".',
225
+ inputSchema: {}
226
+ },
227
+ async () => run(() => api.get("/smart-lists"))
228
+ );
229
+ server.registerTool(
230
+ "create_list",
231
+ {
232
+ title: "Create list",
233
+ description: "Create a new list.",
234
+ inputSchema: {
235
+ name: z.string(),
236
+ color: z.string().optional().describe("hex color e.g. #22c55e"),
237
+ parent: z.string().optional().describe("parent list name or id")
238
+ }
239
+ },
240
+ async ({ name, color, parent }) => run(async () => {
241
+ const payload = { name };
242
+ if (color !== void 0) payload.color = color;
243
+ if (parent !== void 0) payload.parent_id = (await resolveListArg(parent)).id;
244
+ return api.post("/lists", payload);
245
+ })
246
+ );
247
+ server.registerTool(
248
+ "rename_list",
249
+ {
250
+ title: "Rename list",
251
+ description: "Rename a list.",
252
+ inputSchema: { list: z.string().describe("list name or id"), new_name: z.string() }
253
+ },
254
+ async ({ list, new_name }) => run(async () => {
255
+ const target = await resolveListArg(list);
256
+ return api.put(`/lists/${target.id}`, { name: new_name });
257
+ })
258
+ );
259
+ server.registerTool(
260
+ "delete_list",
261
+ {
262
+ title: "Delete list",
263
+ description: "Delete a list and all of its todos. This is irreversible.",
264
+ inputSchema: { list: z.string().describe("list name or id") }
265
+ },
266
+ async ({ list }) => run(async () => {
267
+ const target = await resolveListArg(list);
268
+ await api.del(`/lists/${target.id}`);
269
+ return `Deleted list "${target.name}" (${target.todo_count} todos).`;
270
+ })
271
+ );
272
+ return server;
273
+ }
274
+ async function main() {
275
+ if (!loadCredentials()) {
276
+ process.stderr.write("kaname-mcp: no stored session; run `kaname login` first.\n");
277
+ }
278
+ const server = buildServer();
279
+ await server.connect(new StdioServerTransport());
280
+ }
281
+ main().catch((err) => {
282
+ process.stderr.write(`kaname-mcp failed to start: ${err.message}
283
+ `);
284
+ process.exit(1);
285
+ });
286
+ export {
287
+ buildServer
288
+ };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@kaname-tasks/kaname",
3
+ "version": "0.1.0",
4
+ "description": "Command-line client for Kaname",
5
+ "type": "module",
6
+ "bin": {
7
+ "kaname": "dist/index.js",
8
+ "kaname-mcp": "dist/mcp.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsx src/index.ts",
22
+ "lint": "tsc --noEmit",
23
+ "test": "vitest run",
24
+ "generate-types": "node scripts/generate-types.js"
25
+ },
26
+ "dependencies": {
27
+ "@clack/prompts": "^0.11.0",
28
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
+ "commander": "^14.0.0",
30
+ "picocolors": "^1.1.1",
31
+ "zod": "^4.4.3"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.20.0",
35
+ "openapi-typescript": "^7.10.1",
36
+ "tsup": "^8.3.5",
37
+ "tsx": "^4.19.2",
38
+ "typescript": "^5.7.2",
39
+ "vitest": "^3.0.0"
40
+ }
41
+ }