@kaname-tasks/kaname 0.1.0 → 0.2.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 +38 -9
- package/dist/{chunk-T7KF3F3M.js → chunk-FR675V3R.js} +55 -3
- package/dist/index.js +16 -39
- package/dist/mcp.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,19 @@ Command-line client for Kaname. See `docs/cli-plan.md` for the design.
|
|
|
5
5
|
The package ships two binaries: `kaname` (the CLI) and `kaname-mcp` (an MCP
|
|
6
6
|
server exposing the same operations as tools — see [MCP server](#mcp-server)).
|
|
7
7
|
|
|
8
|
-
## Install
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g @kaname-tasks/kaname # installs the `kaname` and `kaname-mcp` binaries
|
|
12
|
+
# or run without installing:
|
|
13
|
+
npx @kaname-tasks/kaname ls
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Node.js >= 20. The CLI talks to the hosted backend
|
|
17
|
+
(`https://kaname-api-p0su.onrender.com`) by default — no configuration needed.
|
|
18
|
+
Point it elsewhere with `KANAME_API_URL` or `kaname login --api-url ...`.
|
|
19
|
+
|
|
20
|
+
### Install from the monorepo (for development)
|
|
9
21
|
|
|
10
22
|
```bash
|
|
11
23
|
cd cli
|
|
@@ -14,6 +26,12 @@ npm run build
|
|
|
14
26
|
npm link # makes `kaname` and `kaname-mcp` available on your PATH
|
|
15
27
|
```
|
|
16
28
|
|
|
29
|
+
For local development against a backend on your machine, override the default:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
export KANAME_API_URL=http://localhost:8000
|
|
33
|
+
```
|
|
34
|
+
|
|
17
35
|
## Login
|
|
18
36
|
|
|
19
37
|
```bash
|
|
@@ -43,15 +61,15 @@ kaname ls --all
|
|
|
43
61
|
kaname ls --list groceries
|
|
44
62
|
kaname ls --smart errands
|
|
45
63
|
|
|
46
|
-
kaname show
|
|
47
|
-
kaname done
|
|
48
|
-
kaname undone
|
|
49
|
-
kaname edit
|
|
50
|
-
kaname rm
|
|
64
|
+
kaname show 4f2a1c # short ids are shown by `kaname ls` and never change
|
|
65
|
+
kaname done 4f2a1c 9bd003 # complete several at once
|
|
66
|
+
kaname undone 9bd003
|
|
67
|
+
kaname edit 4f2a1c --when tomorrow --list none
|
|
68
|
+
kaname rm 4f2a1c # asks for confirmation (or --force)
|
|
51
69
|
|
|
52
|
-
kaname subtask add
|
|
53
|
-
kaname subtask done
|
|
54
|
-
kaname subtask rm
|
|
70
|
+
kaname subtask add 4f2a1c whole milk
|
|
71
|
+
kaname subtask done 4f2a1c 1
|
|
72
|
+
kaname subtask rm 4f2a1c 2
|
|
55
73
|
|
|
56
74
|
kaname list ls
|
|
57
75
|
kaname list add groceries --color '#22c55e'
|
|
@@ -65,6 +83,17 @@ kaname logout
|
|
|
65
83
|
Dates accept `today`, `tomorrow`, weekday names (`fri`), offsets (`+3d`), ISO
|
|
66
84
|
(`2026-07-10`), and `none` to clear a date in `edit`.
|
|
67
85
|
|
|
86
|
+
Any command taking a todo accepts three ways to name one:
|
|
87
|
+
|
|
88
|
+
| Form | Example | Notes |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| short id | `4f2a1c` | Shown by `ls`; the leading chars of the todo's uuid, so it never changes |
|
|
91
|
+
| `#`-index | `#2` or `2` | Position in the **last** `ls` output — convenient, but shifts whenever you re-list |
|
|
92
|
+
| full uuid | `4f2a1c…` | What `--json` and `--plain` print |
|
|
93
|
+
|
|
94
|
+
Prefer the short id: an index means whatever the most recent `ls` in *any* shell
|
|
95
|
+
put in that row, so it goes stale the moment you list something else.
|
|
96
|
+
|
|
68
97
|
Every read command takes `--json` (raw API response, for `jq`/scripts) and
|
|
69
98
|
`--plain` (tab-separated, no color). Exit codes: `1` for errors, `2` for
|
|
70
99
|
"not logged in / session expired".
|
|
@@ -36,6 +36,11 @@ function saveConfig(config) {
|
|
|
36
36
|
writeJson(configFile(), config);
|
|
37
37
|
}
|
|
38
38
|
var DEFAULT_API_URL = "https://kaname-api-p0su.onrender.com";
|
|
39
|
+
var DEFAULT_GOOGLE_CLIENT_ID = "191990914988-0hmfjva1fe1adq6p8bi7qd713jskb5hv.apps.googleusercontent.com";
|
|
40
|
+
var DEFAULT_GOOGLE_CLIENT_SECRET = Buffer.from(
|
|
41
|
+
"R09DU1BYLVlHdlFYeWpseUYzZEUwMzQwbUZhaU9pZWdYbUE=",
|
|
42
|
+
"base64"
|
|
43
|
+
).toString("utf8");
|
|
39
44
|
function apiUrl() {
|
|
40
45
|
return process.env.KANAME_API_URL || loadConfig().apiUrl || DEFAULT_API_URL;
|
|
41
46
|
}
|
|
@@ -194,6 +199,50 @@ async function confirmOrFail(message) {
|
|
|
194
199
|
|
|
195
200
|
// src/util/output.ts
|
|
196
201
|
import pc from "picocolors";
|
|
202
|
+
|
|
203
|
+
// src/util/ids.ts
|
|
204
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
205
|
+
var SHORT_ID_LEN = 6;
|
|
206
|
+
function shortId(id) {
|
|
207
|
+
return id.slice(0, SHORT_ID_LEN);
|
|
208
|
+
}
|
|
209
|
+
function matchPrefix(ids, prefix) {
|
|
210
|
+
const lower = prefix.toLowerCase();
|
|
211
|
+
return ids.filter((id) => id.toLowerCase().startsWith(lower));
|
|
212
|
+
}
|
|
213
|
+
function ambiguous(token, count) {
|
|
214
|
+
return new Error(`Ambiguous id "${token}" (${count} matches) \u2014 use more characters.`);
|
|
215
|
+
}
|
|
216
|
+
async function resolveTodoId(token) {
|
|
217
|
+
const indexMatch = token.match(/^#?(\d{1,4})$/);
|
|
218
|
+
if (indexMatch) {
|
|
219
|
+
const ids = loadLastView();
|
|
220
|
+
const index = Number(indexMatch[1]);
|
|
221
|
+
if (index < 1 || index > ids.length) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
ids.length === 0 ? `No cached listing \u2014 run \`kaname ls\` first, or pass a short id.` : `#${index} is out of range (last listing had ${ids.length} items).`
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return ids[index - 1];
|
|
227
|
+
}
|
|
228
|
+
if (UUID_RE.test(token)) return token;
|
|
229
|
+
if (/^[0-9a-f]{4,}$/i.test(token)) {
|
|
230
|
+
const cached = matchPrefix(loadLastView(), token);
|
|
231
|
+
if (cached.length === 1) return cached[0];
|
|
232
|
+
if (cached.length > 1) throw ambiguous(token, cached.length);
|
|
233
|
+
const todos = await api.get("/todos?all=true");
|
|
234
|
+
const matches = matchPrefix(
|
|
235
|
+
todos.map((t) => t.id),
|
|
236
|
+
token
|
|
237
|
+
);
|
|
238
|
+
if (matches.length === 1) return matches[0];
|
|
239
|
+
if (matches.length > 1) throw ambiguous(token, matches.length);
|
|
240
|
+
throw new Error(`No todo has id "${token}".`);
|
|
241
|
+
}
|
|
242
|
+
throw new Error(`"${token}" is not a #index, short id, or UUID.`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/util/output.ts
|
|
197
246
|
function printJson(data) {
|
|
198
247
|
console.log(JSON.stringify(data, null, 2));
|
|
199
248
|
}
|
|
@@ -237,7 +286,7 @@ function printTodos(todos, opts, listNames) {
|
|
|
237
286
|
const index = pc.dim(`#${i + 1}`.padStart(indexWidth + 1));
|
|
238
287
|
const mark = t.completed ? pc.green("\u2713") : "\u25CB";
|
|
239
288
|
const title = t.completed ? pc.dim(t.title) : t.title;
|
|
240
|
-
const parts = [index, mark, title];
|
|
289
|
+
const parts = [index, pc.dim(shortId(t.id)), mark, title];
|
|
241
290
|
const when = colorDate(dateLabel(t.when_date, t.when_time), t.when_date, t.completed);
|
|
242
291
|
if (when) parts.push(when);
|
|
243
292
|
if (t.due_date) {
|
|
@@ -257,7 +306,7 @@ function printTodoDetail(todo, opts) {
|
|
|
257
306
|
return;
|
|
258
307
|
}
|
|
259
308
|
console.log(`${todo.completed ? pc.green("\u2713") : "\u25CB"} ${pc.bold(todo.title)}`);
|
|
260
|
-
console.log(pc.dim(` id: ${todo.id}`));
|
|
309
|
+
console.log(pc.dim(` id: ${shortId(todo.id)} (${todo.id})`));
|
|
261
310
|
if (todo.description) console.log(` ${todo.description}`);
|
|
262
311
|
if (todo.when_date) console.log(` when: ${colorDate(dateLabel(todo.when_date, todo.when_time), todo.when_date, todo.completed)}`);
|
|
263
312
|
if (todo.due_date) console.log(` due: ${colorDate(dateLabel(todo.due_date, todo.due_time), todo.due_date, todo.completed)}`);
|
|
@@ -359,18 +408,21 @@ function registerListCommands(program) {
|
|
|
359
408
|
export {
|
|
360
409
|
loadConfig,
|
|
361
410
|
saveConfig,
|
|
411
|
+
DEFAULT_GOOGLE_CLIENT_ID,
|
|
412
|
+
DEFAULT_GOOGLE_CLIENT_SECRET,
|
|
362
413
|
apiUrl,
|
|
363
414
|
loadCredentials,
|
|
364
415
|
saveCredentials,
|
|
365
416
|
clearCredentials,
|
|
366
417
|
saveLastView,
|
|
367
|
-
loadLastView,
|
|
368
418
|
ApiError,
|
|
369
419
|
AuthError,
|
|
370
420
|
api,
|
|
371
421
|
confirmOrFail,
|
|
372
422
|
localToday,
|
|
373
423
|
parseDateArg,
|
|
424
|
+
shortId,
|
|
425
|
+
resolveTodoId,
|
|
374
426
|
printJson,
|
|
375
427
|
printTodos,
|
|
376
428
|
printTodoDetail,
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ApiError,
|
|
4
4
|
AuthError,
|
|
5
|
+
DEFAULT_GOOGLE_CLIENT_ID,
|
|
6
|
+
DEFAULT_GOOGLE_CLIENT_SECRET,
|
|
5
7
|
api,
|
|
6
8
|
apiUrl,
|
|
7
9
|
clearCredentials,
|
|
@@ -9,7 +11,6 @@ import {
|
|
|
9
11
|
fetchLists,
|
|
10
12
|
listNameMap,
|
|
11
13
|
loadConfig,
|
|
12
|
-
loadLastView,
|
|
13
14
|
localToday,
|
|
14
15
|
parseDateArg,
|
|
15
16
|
printJson,
|
|
@@ -17,10 +18,12 @@ import {
|
|
|
17
18
|
printTodos,
|
|
18
19
|
registerListCommands,
|
|
19
20
|
resolveList,
|
|
21
|
+
resolveTodoId,
|
|
20
22
|
saveConfig,
|
|
21
23
|
saveCredentials,
|
|
22
|
-
saveLastView
|
|
23
|
-
|
|
24
|
+
saveLastView,
|
|
25
|
+
shortId
|
|
26
|
+
} from "./chunk-FR675V3R.js";
|
|
24
27
|
|
|
25
28
|
// src/index.ts
|
|
26
29
|
import { Command } from "commander";
|
|
@@ -45,8 +48,8 @@ function openBrowser(url) {
|
|
|
45
48
|
}
|
|
46
49
|
function googleOAuthConfig() {
|
|
47
50
|
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;
|
|
51
|
+
const clientId = process.env.KANAME_GOOGLE_CLIENT_ID || config.googleClientId || DEFAULT_GOOGLE_CLIENT_ID;
|
|
52
|
+
const clientSecret = process.env.KANAME_GOOGLE_CLIENT_SECRET || config.googleClientSecret || DEFAULT_GOOGLE_CLIENT_SECRET;
|
|
50
53
|
if (!clientId || !clientSecret) {
|
|
51
54
|
throw new Error(
|
|
52
55
|
'Google OAuth is not configured. Set KANAME_GOOGLE_CLIENT_ID / KANAME_GOOGLE_CLIENT_SECRET (a "Desktop app" OAuth client), or use `kaname login --password`.'
|
|
@@ -183,32 +186,6 @@ function registerAuthCommands(program2) {
|
|
|
183
186
|
|
|
184
187
|
// src/commands/todos.ts
|
|
185
188
|
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
189
|
async function buildEditPayload(opts) {
|
|
213
190
|
const payload = {};
|
|
214
191
|
if (opts.title !== void 0) payload.title = opts.title;
|
|
@@ -230,7 +207,7 @@ function registerTodoCommands(program2) {
|
|
|
230
207
|
const todo = await api.post("/todos", payload);
|
|
231
208
|
const globalOpts = program2.opts();
|
|
232
209
|
if (globalOpts.json) printJson(todo);
|
|
233
|
-
else console.log(pc2.green("Added:") + ` ${todo.title} ${pc2.dim(todo.id)}`);
|
|
210
|
+
else console.log(pc2.green("Added:") + ` ${todo.title} ${pc2.dim(shortId(todo.id))}`);
|
|
234
211
|
});
|
|
235
212
|
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
213
|
async (opts) => {
|
|
@@ -271,18 +248,18 @@ function registerTodoCommands(program2) {
|
|
|
271
248
|
}
|
|
272
249
|
);
|
|
273
250
|
program2.command("show <id>").description("Show a todo with its subtasks").action(async (idToken) => {
|
|
274
|
-
const todo = await api.get(`/todos/${resolveTodoId(idToken)}`);
|
|
251
|
+
const todo = await api.get(`/todos/${await resolveTodoId(idToken)}`);
|
|
275
252
|
printTodoDetail(todo, program2.opts());
|
|
276
253
|
});
|
|
277
254
|
program2.command("done <ids...>").description("Complete one or more todos").action(async (idTokens) => {
|
|
278
255
|
for (const token of idTokens) {
|
|
279
|
-
const todo = await api.put(`/todos/${resolveTodoId(token)}`, { completed: true });
|
|
256
|
+
const todo = await api.put(`/todos/${await resolveTodoId(token)}`, { completed: true });
|
|
280
257
|
console.log(`${pc2.green("\u2713")} ${todo.title}`);
|
|
281
258
|
}
|
|
282
259
|
});
|
|
283
260
|
program2.command("undone <ids...>").description("Reopen one or more completed todos").action(async (idTokens) => {
|
|
284
261
|
for (const token of idTokens) {
|
|
285
|
-
const todo = await api.put(`/todos/${resolveTodoId(token)}`, { completed: false });
|
|
262
|
+
const todo = await api.put(`/todos/${await resolveTodoId(token)}`, { completed: false });
|
|
286
263
|
console.log(`\u25CB ${todo.title}`);
|
|
287
264
|
}
|
|
288
265
|
});
|
|
@@ -291,11 +268,11 @@ function registerTodoCommands(program2) {
|
|
|
291
268
|
if (Object.keys(payload).length === 0) {
|
|
292
269
|
throw new Error("Nothing to change \u2014 pass at least one of --title/--notes/--when/--due/--list.");
|
|
293
270
|
}
|
|
294
|
-
const todo = await api.put(`/todos/${resolveTodoId(idToken)}`, payload);
|
|
271
|
+
const todo = await api.put(`/todos/${await resolveTodoId(idToken)}`, payload);
|
|
295
272
|
printTodoDetail(todo, program2.opts());
|
|
296
273
|
});
|
|
297
274
|
program2.command("rm <id>").description("Delete a todo").option("-f, --force", "skip confirmation").action(async (idToken, opts) => {
|
|
298
|
-
const id = resolveTodoId(idToken);
|
|
275
|
+
const id = await resolveTodoId(idToken);
|
|
299
276
|
const todo = await api.get(`/todos/${id}`);
|
|
300
277
|
if (!opts.force && !await confirmOrFail(`Delete "${todo.title}"?`)) return;
|
|
301
278
|
await api.del(`/todos/${id}`);
|
|
@@ -304,13 +281,13 @@ function registerTodoCommands(program2) {
|
|
|
304
281
|
const subtask = program2.command("subtask").description("Manage subtasks of a todo");
|
|
305
282
|
subtask.command("add <id> <title...>").description("Add a subtask").action(async (idToken, titleWords) => {
|
|
306
283
|
const created = await api.post(
|
|
307
|
-
`/todos/${resolveTodoId(idToken)}/subtasks`,
|
|
284
|
+
`/todos/${await resolveTodoId(idToken)}/subtasks`,
|
|
308
285
|
{ title: titleWords.join(" ") }
|
|
309
286
|
);
|
|
310
287
|
console.log(pc2.green("Added subtask:") + ` ${created.title}`);
|
|
311
288
|
});
|
|
312
289
|
const resolveSubtask = async (idToken, indexArg) => {
|
|
313
|
-
const todoId = resolveTodoId(idToken);
|
|
290
|
+
const todoId = await resolveTodoId(idToken);
|
|
314
291
|
const todo = await api.get(`/todos/${todoId}`);
|
|
315
292
|
const subtasks = todo.subtasks ?? [];
|
|
316
293
|
const index = Number(indexArg);
|
package/dist/mcp.js
CHANGED