@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 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 (from the monorepo)
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 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)
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 1 whole milk
53
- kaname subtask done 1 1
54
- kaname subtask rm 1 2
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
- } from "./chunk-T7KF3F3M.js";
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
@@ -9,7 +9,7 @@ import {
9
9
  localToday,
10
10
  parseDateArg,
11
11
  resolveList
12
- } from "./chunk-T7KF3F3M.js";
12
+ } from "./chunk-FR675V3R.js";
13
13
 
14
14
  // src/mcp.ts
15
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaname-tasks/kaname",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line client for Kaname",
5
5
  "type": "module",
6
6
  "bin": {