keepsake-mcp 1.8.0 → 1.9.1

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.
Files changed (3) hide show
  1. package/README.md +14 -6
  2. package/build/tools.js +110 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP server for [Keepsake](https://keepsake.place) — the personal CRM that helps you nurture your relationships.
4
4
 
5
- Connect your AI assistant (Claude, Cursor, or any MCP-compatible client) to your Keepsake data: contacts, interactions, tasks, notes, daily journal, companies, and tags.
5
+ Connect your AI assistant (Claude, Cursor, or any MCP-compatible client) to your Keepsake data: contacts, interactions, tasks, notes, daily intentions, companies, and tags.
6
6
 
7
7
  ## Why
8
8
 
@@ -114,7 +114,7 @@ rather than in the chat, which disappears.
114
114
  |--------|-----------|-------------|
115
115
  | `review_note` | `note_id` | Act as the editor of a note: read it, judge form and substance, leave anchored remarks in the margin, never rewrite the text |
116
116
 
117
- ## Available tools (67)
117
+ ## Available tools (71)
118
118
 
119
119
  ### Contacts
120
120
  | Tool | Description |
@@ -181,12 +181,20 @@ Material kept *alongside* a note without entering its text — an idea, a refere
181
181
  | `update_note_comment` | Edit the content of a marginalia |
182
182
  | `delete_note_comment` | Permanently delete a marginalia |
183
183
 
184
- ### Daily Journal
184
+ ### Days (intention or question of the day)
185
185
  | Tool | Description |
186
186
  |------|-------------|
187
- | `list_days` | List journal entries by date range |
188
- | `get_day` | Get a specific day's journal |
189
- | `update_day` | Create or update a day's journal (upsert) |
187
+ | `list_days` | List days with their intention or question of the day, by date range |
188
+ | `get_day` | Get a day and its intention or question (field `note`) |
189
+ | `update_day` | Set a day's intention or question — one short line, not a journal (upsert) |
190
+
191
+ ### Day blocks (Day-view timeline)
192
+ | Tool | Description |
193
+ |------|-------------|
194
+ | `list_day_blocks` | List a day's time blocks, in timeline order |
195
+ | `create_day_block` | Create a block, auto-placed first-fit (or pinned via anchor_time) |
196
+ | `update_day_block` | Update a block (title, duration, anchor, note, done) |
197
+ | `delete_day_block` | Delete a block and prune its timeline ref |
190
198
 
191
199
  ### Tags
192
200
  | Tool | Description |
package/build/tools.js CHANGED
@@ -7,8 +7,14 @@ export function toContent(result) {
7
7
  : result.error.message || JSON.stringify(result.error);
8
8
  return { content: [{ type: "text", text: `Error: ${msg}` }] };
9
9
  }
10
+ // List endpoints ship pagination alongside the rows (meta.total / limit /
11
+ // offset). Dropping it forced agents to binary-search offsets just to count
12
+ // an inbox — keep data and meta together whenever meta is present.
13
+ const payload = result.meta !== undefined
14
+ ? { data: result.data ?? [], meta: result.meta }
15
+ : (result.data ?? result);
10
16
  return {
11
- content: [{ type: "text", text: JSON.stringify(result.data ?? result, null, 2) }],
17
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
12
18
  };
13
19
  }
14
20
  /** Build query string from optional params, skipping undefined values. */
@@ -358,6 +364,18 @@ export function registerAllTools(server, fetchApi) {
358
364
  .positive()
359
365
  .optional()
360
366
  .describe("Recurrence interval (e.g., every N days)"),
367
+ start_time: z
368
+ .string()
369
+ .regex(/^([01]\d|2[0-3]):[0-5]\d$/)
370
+ .optional()
371
+ .describe("Wall-clock start time HH:MM (24h). A task of the day WITH a time appears anchored on the Day-view timeline; without one it stays in the day's task list."),
372
+ duration_minutes: z
373
+ .number()
374
+ .int()
375
+ .min(1)
376
+ .max(1440)
377
+ .optional()
378
+ .describe("Estimated duration in minutes (Day-view timeline shows 15 by default when a start time is set)"),
361
379
  contact_ids: z
362
380
  .array(z.string().uuid())
363
381
  .optional()
@@ -383,6 +401,20 @@ export function registerAllTools(server, fetchApi) {
383
401
  .optional()
384
402
  .describe("Date type: specific (has a due date), asap (do as soon as possible), one_day (someday/no rush)"),
385
403
  priority: z.enum(["low", "medium", "high"]).optional().describe("Priority level"),
404
+ start_time: z
405
+ .string()
406
+ .regex(/^([01]\d|2[0-3]):[0-5]\d$/)
407
+ .nullable()
408
+ .optional()
409
+ .describe("Wall-clock start time HH:MM (24h) — anchors the task on the Day-view timeline. Pass null to clear it (unschedule)."),
410
+ duration_minutes: z
411
+ .number()
412
+ .int()
413
+ .min(1)
414
+ .max(1440)
415
+ .nullable()
416
+ .optional()
417
+ .describe("Estimated duration in minutes. Pass null to clear."),
386
418
  contact_ids: z
387
419
  .array(z.string().uuid())
388
420
  .optional()
@@ -535,7 +567,7 @@ export function registerAllTools(server, fetchApi) {
535
567
  // DAYS (Daily Summaries)
536
568
  // ===========================================================================
537
569
  server.registerTool("list_days", {
538
- description: "List daily journal summaries. Filter by date range.",
570
+ description: "List days with their intention or question of the day (field `note`). Filter by date range.",
539
571
  inputSchema: {
540
572
  from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
541
573
  to: z.string().optional().describe("End date (YYYY-MM-DD)"),
@@ -547,7 +579,7 @@ export function registerAllTools(server, fetchApi) {
547
579
  return toContent(await fetchApi(`/days${qs({ from, to, limit, offset })}`));
548
580
  });
549
581
  server.registerTool("get_day", {
550
- description: "Get a specific day's journal summary by date.",
582
+ description: "Get a specific day by date, including its intention or question of the day (field `note`).",
551
583
  inputSchema: {
552
584
  date: z.string().describe("Date (YYYY-MM-DD)"),
553
585
  },
@@ -556,16 +588,89 @@ export function registerAllTools(server, fetchApi) {
556
588
  return toContent(await fetchApi(`/days/${date}`));
557
589
  });
558
590
  server.registerTool("update_day", {
559
- description: "Create or update a daily journal summary. If a day entry already exists for this date, it will be updated (upsert).",
591
+ description: "Create or update a day's intention or question of the day (upsert on the date). The `note` field is the intention or question of the day — one short line at the top of the Today view (a mantra, an intention, a single priority, or a question to keep in mind). Not a journal: never write a summary of the day here.",
560
592
  inputSchema: {
561
593
  date: z.string().describe("Date (YYYY-MM-DD)"),
562
- note: z.string().describe("Journal content for the day"),
594
+ note: z.string().describe("The intention or question of the day: one short line (mantra, intention, single priority, or a question to keep in mind). Not a journal summary."),
563
595
  },
564
596
  annotations: { title: "Update day", destructiveHint: false, idempotentHint: true, openWorldHint: false },
565
597
  }, async (params) => {
566
598
  return toContent(await fetchApi("/days", "POST", params));
567
599
  });
568
600
  // ===========================================================================
601
+ // DAY BLOCKS (Day-view timeline)
602
+ // ===========================================================================
603
+ server.registerTool("list_day_blocks", {
604
+ description: "List the time blocks of a day's timeline (Day view), in timeline order. A block is either an activity (type 'block') or a task bucket (type 'tasks' — groups the day's untimed tasks). The meta carries the day's raw timeline_order: \"b:<uuid>\" refs are blocks, bare uuids are tasks inside a bucket's run.",
605
+ inputSchema: {
606
+ date: z.string().describe("Date (YYYY-MM-DD)"),
607
+ },
608
+ annotations: { title: "List day blocks", readOnlyHint: true, openWorldHint: false },
609
+ }, async ({ date }) => {
610
+ return toContent(await fetchApi(`/day-blocks${qs({ date })}`));
611
+ });
612
+ server.registerTool("create_day_block", {
613
+ description: "Create a time block on a day's timeline. The block is auto-placed in the earliest free gap large enough for it (same first-fit engine as the app); pass anchor_time to pin it at a fixed hour instead. Its \"b:<id>\" ref is inserted into the day's timeline_order for you — never write timeline_order by hand.",
614
+ inputSchema: {
615
+ date: z.string().describe("Date (YYYY-MM-DD)"),
616
+ title: z.string().optional().describe("Block label (e.g. \"Deep work\", \"Lunch\")"),
617
+ type: z
618
+ .enum(["block", "tasks"])
619
+ .optional()
620
+ .describe("'block' = activity (default). 'tasks' = task bucket: a window that gathers the day's untimed tasks."),
621
+ duration_minutes: z
622
+ .number()
623
+ .int()
624
+ .min(1)
625
+ .max(1440)
626
+ .optional()
627
+ .describe("Duration in minutes (default 30 for an activity, 60 for a bucket)"),
628
+ anchor_time: z
629
+ .string()
630
+ .regex(/^([01]\d|2[0-3]):[0-5]\d$/)
631
+ .optional()
632
+ .describe("Pin the block at a fixed wall-clock hour HH:MM (it becomes a wall other blocks flow around). Omit for automatic first-fit placement."),
633
+ note: z.string().optional().describe("Free note attached to the block"),
634
+ },
635
+ annotations: { title: "Create day block", destructiveHint: false, idempotentHint: false, openWorldHint: false },
636
+ }, async (params) => {
637
+ return toContent(await fetchApi("/day-blocks", "POST", params));
638
+ });
639
+ server.registerTool("update_day_block", {
640
+ description: "Update a time block (title, duration, anchor, note, done). Only send fields you want to change. The block keeps its position in the timeline order.",
641
+ inputSchema: {
642
+ id: z.string().uuid().describe("Block UUID"),
643
+ title: z.string().nullable().optional().describe("Block label"),
644
+ duration_minutes: z
645
+ .number()
646
+ .int()
647
+ .min(1)
648
+ .max(1440)
649
+ .optional()
650
+ .describe("Duration in minutes"),
651
+ anchor_time: z
652
+ .string()
653
+ .regex(/^([01]\d|2[0-3]):[0-5]\d$/)
654
+ .nullable()
655
+ .optional()
656
+ .describe("Fixed wall-clock hour HH:MM. Pass null to unpin (the block flows with the rest again)."),
657
+ note: z.string().nullable().optional().describe("Free note. Pass null to clear."),
658
+ done: z.boolean().optional().describe("Mark the block done (true) or not done (false)"),
659
+ },
660
+ annotations: { title: "Update day block", destructiveHint: false, idempotentHint: true, openWorldHint: false },
661
+ }, async ({ id, ...body }) => {
662
+ return toContent(await fetchApi(`/day-blocks/${id}`, "PATCH", body));
663
+ });
664
+ server.registerTool("delete_day_block", {
665
+ description: "Delete a time block. Its ref is pruned from the day's timeline_order; for a task bucket, the tasks themselves are untouched (they fall back to the day's computed bucket).",
666
+ inputSchema: {
667
+ id: z.string().uuid().describe("Block UUID"),
668
+ },
669
+ annotations: { title: "Delete day block", destructiveHint: true, idempotentHint: true, openWorldHint: false },
670
+ }, async ({ id }) => {
671
+ return toContent(await fetchApi(`/day-blocks/${id}`, "DELETE"));
672
+ });
673
+ // ===========================================================================
569
674
  // TAGS
570
675
  // ===========================================================================
571
676
  server.registerTool("list_tags", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keepsake-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "MCP server for Keepsake personal CRM \u2014 connect your AI agent to your contacts, tasks, notes, and more",
5
5
  "type": "module",
6
6
  "bin": {