keepsake-mcp 1.12.0 → 1.14.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
@@ -143,7 +143,7 @@ rather than in the chat, which disappears.
143
143
  ### Entries (Interactions)
144
144
  | Tool | Description |
145
145
  |------|-------------|
146
- | `list_entries` | List interactions (calls, emails, meetings, etc.) |
146
+ | `list_entries` | List interactions (calls, emails, meetings, etc.) — filter by type, contact, company, page (`tag_id`), dates |
147
147
  | `create_entry` | Log a new interaction — supports `#tag#` and `[[tag]]` syntax |
148
148
  | `update_entry` | Update an interaction |
149
149
  | `delete_entry` | Delete an interaction |
@@ -151,7 +151,7 @@ rather than in the chat, which disappears.
151
151
  ### Tasks
152
152
  | Tool | Description |
153
153
  |------|-------------|
154
- | `list_tasks` | List tasks with status/date filters |
154
+ | `list_tasks` | List tasks — filter by status, date, company, page (`tag_id`) |
155
155
  | `get_task` | Get a task with its tags, contacts, companies and linked notes |
156
156
  | `create_task` | Create a task — supports `#tag#` and `[[tag]]` syntax |
157
157
  | `update_task` | Update task fields |
@@ -165,12 +165,12 @@ rather than in the chat, which disappears.
165
165
  ### QuickNotes
166
166
  | Tool | Description |
167
167
  |------|-------------|
168
- | `list_notes` | List notes (filter by pinned/archived) |
168
+ | `list_notes` | List notes — filter by pinned/archived, day, company, page (`tag_id`) |
169
169
  | `get_note` | Get one note by ID with its tags, contacts, tasks and linked notes |
170
170
  | `create_note` | Create a note — supports `#tag#` and `[[tag]]` syntax |
171
171
  | `update_note` | Update note content |
172
172
  | `delete_note` | Soft-delete (or permanent) |
173
- | `pin_note` | Pin to top |
173
+ | `pin_note` | Pin as a post-it (short reference always at hand) |
174
174
  | `archive_note` | Archive a note |
175
175
  | `restore_note` | Restore a deleted/archived note |
176
176
 
@@ -188,9 +188,9 @@ Material kept *alongside* a note without entering its text — an idea, a refere
188
188
  ### Days (intention or question of the day)
189
189
  | Tool | Description |
190
190
  |------|-------------|
191
- | `list_days` | List days with their intention or question of the day, by date range |
192
- | `get_day` | Get a day and its intention or question (field `note`) |
193
- | `update_day` | Set a day's intention or question — one short line, not a journal (upsert) |
191
+ | `list_days` | List days with their intention or question of the day (the one in force, which stays until changed), by date range |
192
+ | `get_day` | Get a day, its intention in force (`intention`, carried over until changed) and what was written that day (`note`) |
193
+ | `update_day` | Set a day's intention or question — one short line, not a journal (upsert); it stays in place on the following days until changed |
194
194
 
195
195
  ### Day blocks (Day-view timeline)
196
196
  | Tool | Description |
@@ -206,7 +206,7 @@ Material kept *alongside* a note without entering its text — an idea, a refere
206
206
  | `list_tags` | List tags (lightweight — ordering arrays omitted), with optional name search (`q`) |
207
207
  | `get_tag` | Get a tag by ID with all properties, including `tasks_order` (section markers `h:<header_id>`) |
208
208
  | `create_tag` | Create a new tag |
209
- | `update_tag` | Update a tag (name, description, color, icon, view mode, favorite) |
209
+ | `update_tag` | Update a tag (name, description, color, icon, favorite, order of tasks and sections via `tasks_order`) |
210
210
  | `delete_tag` | Permanently delete a tag and all its links |
211
211
  | `get_tag_items` | Get items linked to a tag — filter by `types`/`status`, `summary` mode, task `sections` included |
212
212
  | `link_tag` | Link any entity to a tag |
@@ -217,7 +217,7 @@ Material kept *alongside* a note without entering its text — an idea, a refere
217
217
  |------|-------------|
218
218
  | `list_task_headers` | List all task headers (section separators) |
219
219
  | `get_task_header` | Get a task header by ID |
220
- | `create_task_header` | Create a task header (section) |
220
+ | `create_task_header` | Create a task header (section) — insert `h:<id>` in the tag's `tasks_order` to place it |
221
221
  | `update_task_header` | Update a task header (name, description, collapsed) |
222
222
  | `delete_task_header` | Permanently delete a task header |
223
223
 
package/build/tools.js CHANGED
@@ -295,7 +295,7 @@ export function registerAllTools(server, fetchApi) {
295
295
  // ENTRIES (Interactions)
296
296
  // ===========================================================================
297
297
  server.registerTool("list_entries", {
298
- description: "List interaction entries (calls, emails, meetings, events, etc.). Supports filtering by type, contact, and date range.",
298
+ description: "List interaction entries (calls, emails, meetings, events, etc.). Supports filtering by type, contact, company, tag (page) and date range.",
299
299
  inputSchema: {
300
300
  type: z
301
301
  .enum(["call", "email", "meeting", "event", "gift", "letter", "message", "log", "other"])
@@ -303,14 +303,15 @@ export function registerAllTools(server, fetchApi) {
303
303
  .describe("Filter by entry type"),
304
304
  contact_id: z.string().uuid().optional().describe("Filter by associated contact ID"),
305
305
  company_id: z.string().uuid().optional().describe("Only entries where this company is a participant"),
306
+ tag_id: z.string().uuid().optional().describe("Only entries linked to this tag (page)"),
306
307
  from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
307
308
  to: z.string().optional().describe("End date (YYYY-MM-DD)"),
308
309
  limit: z.number().int().positive().optional().describe("Max results (default 20)"),
309
310
  offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
310
311
  },
311
312
  annotations: { title: "List entries", readOnlyHint: true, openWorldHint: false },
312
- }, async ({ type, contact_id, company_id, from, to, limit, offset }) => {
313
- return toContent(await fetchApi(`/entries${qs({ type, contact_id, company_id, from, to, limit, offset })}`));
313
+ }, async ({ type, contact_id, company_id, tag_id, from, to, limit, offset }) => {
314
+ return toContent(await fetchApi(`/entries${qs({ type, contact_id, company_id, tag_id, from, to, limit, offset })}`));
314
315
  });
315
316
  server.registerTool("create_entry", {
316
317
  description: "An ENTRY is a dated interaction log tied to contacts. Records something that happened (call, meeting, email…) on a specific date.\n\nCreate a new interaction entry. Content supports #tag# and [[tag]] syntax for automatic tag linking.",
@@ -377,7 +378,7 @@ export function registerAllTools(server, fetchApi) {
377
378
  // TASKS
378
379
  // ===========================================================================
379
380
  server.registerTool("list_tasks", {
380
- description: "List tasks. Filter by status (pending/completed), date_type, or specific date.",
381
+ description: "List tasks. Filter by status (pending/completed), date_type, specific date, company, or tag (page) — e.g. tag_id + status=pending lists a project's open tasks.",
381
382
  inputSchema: {
382
383
  status: z.enum(["pending", "completed"]).optional().describe("Filter by status"),
383
384
  date_type: z
@@ -386,12 +387,13 @@ export function registerAllTools(server, fetchApi) {
386
387
  .describe("Filter by date type: specific (has a due date), asap (do as soon as possible), one_day (someday/no rush)"),
387
388
  date: z.string().optional().describe("Filter by specific date (YYYY-MM-DD)"),
388
389
  company_id: z.string().uuid().optional().describe("Only tasks linked directly to this company"),
390
+ tag_id: z.string().uuid().optional().describe("Only tasks linked to this tag (page)"),
389
391
  limit: z.number().int().positive().optional().describe("Max results (default 20)"),
390
392
  offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
391
393
  },
392
394
  annotations: { title: "List tasks", readOnlyHint: true, openWorldHint: false },
393
- }, async ({ status, date_type, date, company_id, limit, offset }) => {
394
- return toContent(await fetchApi(`/tasks${qs({ status, date_type, date, company_id, limit, offset })}`));
395
+ }, async ({ status, date_type, date, company_id, tag_id, limit, offset }) => {
396
+ return toContent(await fetchApi(`/tasks${qs({ status, date_type, date, company_id, tag_id, limit, offset })}`));
395
397
  });
396
398
  server.registerTool("get_task", {
397
399
  description: "Get a single task by ID, with its tags, linked contacts (contact_ids), linked companies (company_ids) and linked notes (note_ids). Use it when you already know which task you need instead of listing everything.",
@@ -540,7 +542,7 @@ export function registerAllTools(server, fetchApi) {
540
542
  // QUICK NOTES
541
543
  // ===========================================================================
542
544
  server.registerTool("list_notes", {
543
- description: "List notes. QuickNotes (inbox, not yet archived) and Notes (archived, permanent). Filter by pinned or archived status, or by the day(s) a note is linked to (`date`, or `date_from`/`date_to`) — e.g. \"what did I note for tomorrow?\". Each note carries `dates`, the days it is linked to.",
545
+ description: "List notes. QuickNotes (inbox, not yet archived) and Notes (archived, permanent). Filter by pinned or archived status, by tag (page) with `tag_id`, or by the day(s) a note is linked to (`date`, or `date_from`/`date_to`) — e.g. \"what did I note for tomorrow?\". Each note carries `dates`, the days it is linked to.",
544
546
  inputSchema: {
545
547
  pinned: z.boolean().optional().describe("Filter pinned notes only"),
546
548
  archived: z.boolean().optional().describe("Filter by status: true = Notes (archived/permanent), false = QuickNotes (inbox)"),
@@ -548,12 +550,13 @@ export function registerAllTools(server, fetchApi) {
548
550
  date_from: z.string().optional().describe("Only notes linked to a day on or after this date (YYYY-MM-DD)"),
549
551
  date_to: z.string().optional().describe("Only notes linked to a day on or before this date (YYYY-MM-DD)"),
550
552
  company_id: z.string().uuid().optional().describe("Only notes linked directly to this company"),
553
+ tag_id: z.string().uuid().optional().describe("Only notes linked to this tag (page)"),
551
554
  limit: z.number().int().positive().optional().describe("Max results (default 20)"),
552
555
  offset: z.number().int().nonnegative().optional().describe("Pagination offset"),
553
556
  },
554
557
  annotations: { title: "List notes", readOnlyHint: true, openWorldHint: false },
555
- }, async ({ pinned, archived, date, date_from, date_to, company_id, limit, offset }) => {
556
- return toContent(await fetchApi(`/notes${qs({ pinned, archived, date, date_from, date_to, company_id, limit, offset })}`));
558
+ }, async ({ pinned, archived, date, date_from, date_to, company_id, tag_id, limit, offset }) => {
559
+ return toContent(await fetchApi(`/notes${qs({ pinned, archived, date, date_from, date_to, company_id, tag_id, limit, offset })}`));
557
560
  });
558
561
  server.registerTool("get_note", {
559
562
  description: "Get a single note by ID, including its tags, linked contacts, linked tasks, linked notes and the days it is linked to (`dates`). Use this when the user points you at one specific note (e.g. gives you its URL — the UUID is the last path segment) instead of listing everything.",
@@ -624,7 +627,7 @@ export function registerAllTools(server, fetchApi) {
624
627
  return toContent(await fetchApi(`/notes/${id}${query}`, "DELETE"));
625
628
  });
626
629
  server.registerTool("pin_note", {
627
- description: "Pin a QuickNote or Note so it appears at the top of the Inbox.",
630
+ description: "Pin a QuickNote or Note so it stays in view. A pin is a POST-IT: a short reference the user wants always at hand (gate code, hotel room number, Wi-Fi password). Do not pin a note just because it is important — meeting prep is linked to its day, project material to a tag, actions become tasks. Pin only when the user asks or the note is clearly such a post-it.",
628
631
  inputSchema: {
629
632
  id: z.string().uuid().describe("Note UUID"),
630
633
  },
@@ -654,7 +657,7 @@ export function registerAllTools(server, fetchApi) {
654
657
  // DAYS (Daily Summaries)
655
658
  // ===========================================================================
656
659
  server.registerTool("list_days", {
657
- description: "List days with their intention or question of the day (field `note`). Filter by date range.",
660
+ description: "List stored days with their intention or question of the day. An intention stays in place until changed: `intention` is the one in force that day (the last one written on that day or before, `intention_date` = when it was written); `note` is what was written on that exact day (null = nothing written). Filter by date range.",
658
661
  inputSchema: {
659
662
  from: z.string().optional().describe("Start date (YYYY-MM-DD)"),
660
663
  to: z.string().optional().describe("End date (YYYY-MM-DD)"),
@@ -666,7 +669,7 @@ export function registerAllTools(server, fetchApi) {
666
669
  return toContent(await fetchApi(`/days${qs({ from, to, limit, offset })}`));
667
670
  });
668
671
  server.registerTool("get_day", {
669
- description: "Get a specific day by date: its intention or question of the day (field `note`, one short line) AND the notes linked to that day (field `notes`, via link_note_date / `dates`). Always 200 — `exists: false` means no intention is stored yet, the linked notes are returned regardless.",
672
+ description: "Get a specific day by date: its intention or question of the day AND the notes linked to that day (field `notes`, via link_note_date / `dates`). An intention stays in place until changed: `intention` is the one in force that day, as the user sees it (the last one written on that day or before; `intention_date` = the day it was written), while `note` is what was written on that exact day (null = nothing written there, '' = intention stopped from that day). Always 200 — `exists: false` means no row is stored for that date; `intention` and the linked notes are returned regardless.",
670
673
  inputSchema: {
671
674
  date: z.string().describe("Date (YYYY-MM-DD)"),
672
675
  },
@@ -675,10 +678,10 @@ export function registerAllTools(server, fetchApi) {
675
678
  return toContent(await fetchApi(`/days/${date}`));
676
679
  });
677
680
  server.registerTool("update_day", {
678
- 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.\n\nNOT for attaching a note to a day: this field is a single line and you would overwrite the user's intention. To put a note on a day, use create_note with `dates` or link_note_date.",
681
+ 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.\n\nAn intention stays in place until changed: writing it on a day replaces the intention from that day onward (following days show it too, until the next day with its own intention — set one on a future date for a one-off, e.g. before a meeting). `null` removes what was written on that day (the day goes back to the intention in force before it); an empty string stops any intention from that day onward. Check `intention` with get_day before writing: you may be replacing a long-running intention the user cares about.\n\nNOT for attaching a note to a day: this field is a single line and you would overwrite the user's intention. To put a note on a day, use create_note with `dates` or link_note_date.",
679
682
  inputSchema: {
680
683
  date: z.string().describe("Date (YYYY-MM-DD)"),
681
- 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."),
684
+ note: z.string().nullable().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. null = remove what was written on this day (back to the previous intention); empty string = stop any intention from this day onward."),
682
685
  },
683
686
  annotations: { title: "Update day", destructiveHint: false, idempotentHint: true, openWorldHint: false },
684
687
  }, async (params) => {
@@ -809,14 +812,17 @@ export function registerAllTools(server, fetchApi) {
809
812
  return toContent(await fetchApi("/tags", "POST", body));
810
813
  });
811
814
  server.registerTool("update_tag", {
812
- description: "Update an existing tag. Only send the fields you want to change. Supports name, description, color, icon, view mode, and favorite status.",
815
+ description: "Update an existing tag. Only send the fields you want to change. Supports name, description, color, icon, favorite status, and the order of the page's tasks and sections (tasks_order).",
813
816
  inputSchema: {
814
817
  id: z.string().uuid().describe("Tag UUID"),
815
818
  name: z.string().optional().describe("Tag name"),
816
819
  description: z.string().optional().describe("Tag description"),
817
820
  color: z.string().optional().describe("Tag color (e.g. 'blue', 'red', 'green')"),
818
821
  icon: z.string().optional().describe("Tag icon (emoji or icon name)"),
819
- view_mode: z.string().optional().describe("View mode for the tag page"),
822
+ tasks_order: z
823
+ .array(z.string())
824
+ .optional()
825
+ .describe("Full display order of the page's tasks: task ids and \"h:<header_id>\" entries for sections, top to bottom. Read the current one with get_tag, insert or move entries, send the whole array back. Tasks listed after a section belong to it."),
820
826
  is_favorite: z.boolean().optional().describe("Whether the tag is a favorite"),
821
827
  },
822
828
  annotations: { title: "Update tag", destructiveHint: false, idempotentHint: true, openWorldHint: false },
@@ -881,7 +887,7 @@ export function registerAllTools(server, fetchApi) {
881
887
  return toContent(await fetchApi(`/task-headers/${id}`));
882
888
  });
883
889
  server.registerTool("create_task_header", {
884
- description: "Create a new task header (section separator). Add it to a tag's items_order to position it.",
890
+ description: "Create a new task header (section separator). To show it on a tag page, insert \"h:<header_id>\" into that tag's tasks_order (get_tag, then update_tag) where the section should start: the tasks listed after it belong to that section.",
885
891
  inputSchema: {
886
892
  name: z.string().describe("Header name"),
887
893
  description: z.string().optional().describe("Optional description below the header"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keepsake-mcp",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
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": {