@knotpad/app 0.1.4 → 0.1.6

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 (340) hide show
  1. package/app/(app)/calendar/page.tsx +57 -0
  2. package/app/(app)/error.tsx +35 -0
  3. package/app/(app)/graph/page.tsx +32 -0
  4. package/app/(app)/guide/page.tsx +21 -0
  5. package/app/(app)/kanban/loading.tsx +24 -0
  6. package/app/(app)/kanban/page.tsx +59 -0
  7. package/app/(app)/layout.tsx +122 -0
  8. package/app/(app)/list/loading.tsx +21 -0
  9. package/app/(app)/list/page.tsx +137 -0
  10. package/app/(app)/loading.tsx +18 -0
  11. package/app/(app)/notes/[noteId]/page.tsx +84 -0
  12. package/app/(app)/notes/layout.tsx +30 -0
  13. package/app/(app)/notes/page.tsx +39 -0
  14. package/app/(app)/page.tsx +5 -0
  15. package/app/(app)/settings/agent-token/page.tsx +59 -0
  16. package/app/(app)/settings/backup/page.tsx +49 -0
  17. package/app/(app)/settings/billing/page.tsx +53 -0
  18. package/app/(app)/settings/calendar/page.tsx +41 -0
  19. package/app/(app)/settings/layout.test.tsx +39 -0
  20. package/app/(app)/settings/layout.tsx +71 -0
  21. package/app/(app)/settings/page.tsx +4 -0
  22. package/app/(app)/settings/security/page.tsx +43 -0
  23. package/app/(app)/settings/team/page.tsx +74 -0
  24. package/app/(app)/settings/workspace/page.tsx +27 -0
  25. package/app/(app)/tasks/[taskId]/page.tsx +79 -0
  26. package/app/(auth)/forgot-password/page.tsx +106 -0
  27. package/app/(auth)/guest/page.tsx +56 -0
  28. package/app/(auth)/layout.tsx +13 -0
  29. package/app/(auth)/login/page.tsx +14 -0
  30. package/app/(auth)/register/page.tsx +193 -0
  31. package/app/(auth)/reset-password/page.tsx +138 -0
  32. package/app/api/account/claim/route.tsx +135 -0
  33. package/app/api/admin/backfill-encryption/route.tsx +43 -0
  34. package/app/api/admin/license/route.tsx +42 -0
  35. package/app/api/auth/2fa/route.tsx +148 -0
  36. package/app/api/auth/[...nextauth]/route.tsx +3 -0
  37. package/app/api/auth/change-password/route.tsx +61 -0
  38. package/app/api/auth/check-2fa/route.tsx +19 -0
  39. package/app/api/auth/forgot-password/route.tsx +65 -0
  40. package/app/api/auth/reset-password/route.tsx +52 -0
  41. package/app/api/auth/verify-2fa/route.tsx +88 -0
  42. package/app/api/backup/download/db/route.ts +29 -0
  43. package/app/api/backup/download/notes/route.ts +25 -0
  44. package/app/api/backup/settings/route.ts +92 -0
  45. package/app/api/billing/checkout/route.tsx +81 -0
  46. package/app/api/billing/migrate/route.tsx +163 -0
  47. package/app/api/billing/portal/route.tsx +24 -0
  48. package/app/api/billing/setup-intent/route.tsx +55 -0
  49. package/app/api/billing/status/route.tsx +36 -0
  50. package/app/api/billing/subscribe/route.tsx +85 -0
  51. package/app/api/billing/webhook/route.tsx +199 -0
  52. package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
  53. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
  54. package/app/api/calendar-feeds/events/route.tsx +82 -0
  55. package/app/api/calendar-feeds/route.tsx +52 -0
  56. package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
  57. package/app/api/cron/calendar-feeds/route.tsx +31 -0
  58. package/app/api/cron/stale-tasks/route.tsx +51 -0
  59. package/app/api/cron/sync/route.tsx +34 -0
  60. package/app/api/devices/[deviceId]/route.tsx +25 -0
  61. package/app/api/devices/route.tsx +41 -0
  62. package/app/api/export/route.tsx +40 -0
  63. package/app/api/feedback/route.tsx +54 -0
  64. package/app/api/folders/[folderId]/route.tsx +51 -0
  65. package/app/api/folders/route.tsx +37 -0
  66. package/app/api/graph/route.tsx +242 -0
  67. package/app/api/guest/route.tsx +58 -0
  68. package/app/api/health/route.tsx +10 -0
  69. package/app/api/holidays/countries/route.tsx +14 -0
  70. package/app/api/holidays/route.tsx +49 -0
  71. package/app/api/holidays/states/route.tsx +21 -0
  72. package/app/api/invites/[token]/route.tsx +131 -0
  73. package/app/api/invites/route.tsx +74 -0
  74. package/app/api/mcp/generate-token/route.tsx +55 -0
  75. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
  76. package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
  77. package/app/api/notes/[noteId]/export/route.tsx +45 -0
  78. package/app/api/notes/[noteId]/route.tsx +360 -0
  79. package/app/api/notes/route.tsx +112 -0
  80. package/app/api/notifications/route.tsx +44 -0
  81. package/app/api/register/route.tsx +67 -0
  82. package/app/api/restore/route.tsx +148 -0
  83. package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
  84. package/app/api/sync/conflicts/route.tsx +48 -0
  85. package/app/api/sync/status/route.tsx +49 -0
  86. package/app/api/sync/trigger/route.tsx +15 -0
  87. package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
  88. package/app/api/tasks/[taskId]/route.tsx +259 -0
  89. package/app/api/tasks/bulk/route.tsx +133 -0
  90. package/app/api/tasks/route.tsx +36 -0
  91. package/app/api/workspace/active/route.tsx +39 -0
  92. package/app/api/workspace/create-team/route.tsx +42 -0
  93. package/app/api/workspace/kanban-statuses/route.tsx +71 -0
  94. package/app/api/workspace/members/[memberId]/route.tsx +69 -0
  95. package/app/api/workspace/route.tsx +24 -0
  96. package/app/download/page.tsx +170 -0
  97. package/app/favicon.ico +0 -0
  98. package/app/generated/prisma/client.d.ts +1 -0
  99. package/app/generated/prisma/client.js +5 -0
  100. package/app/generated/prisma/default.d.ts +1 -0
  101. package/app/generated/prisma/default.js +5 -0
  102. package/app/generated/prisma/edge.d.ts +1 -0
  103. package/app/generated/prisma/edge.js +497 -0
  104. package/app/generated/prisma/index-browser.js +523 -0
  105. package/app/generated/prisma/index.d.ts +46376 -0
  106. package/app/generated/prisma/index.js +497 -0
  107. package/app/generated/prisma/package.json +144 -0
  108. package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
  109. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  110. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
  111. package/app/generated/prisma/runtime/client.d.ts +3386 -0
  112. package/app/generated/prisma/runtime/client.js +86 -0
  113. package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
  114. package/app/generated/prisma/runtime/index-browser.js +6 -0
  115. package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
  116. package/app/generated/prisma/schema.prisma +456 -0
  117. package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
  118. package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
  119. package/app/globals.css +54 -0
  120. package/app/invite/[token]/page.tsx +52 -0
  121. package/app/layout.tsx +90 -0
  122. package/app/mcp/route.tsx +430 -0
  123. package/app/opengraph-image.tsx +120 -0
  124. package/app/page.tsx +398 -0
  125. package/app/privacy/page.tsx +69 -0
  126. package/app/robots.tsx +25 -0
  127. package/app/sitemap.tsx +36 -0
  128. package/app/terms/page.tsx +69 -0
  129. package/app/upgrade/page.tsx +75 -0
  130. package/auth.config.ts +33 -0
  131. package/auth.ts +79 -0
  132. package/bin/brief.js +224 -0
  133. package/components/auth/login-form.tsx +302 -0
  134. package/components/auth/password-checklist.tsx +31 -0
  135. package/components/auth/password-input.tsx +36 -0
  136. package/components/auth/switch-account-button.test.tsx +22 -0
  137. package/components/auth/switch-account-button.tsx +19 -0
  138. package/components/auth/two-factor-input.tsx +116 -0
  139. package/components/billing/billing-dashboard.tsx +265 -0
  140. package/components/billing/card-form.tsx +210 -0
  141. package/components/billing/claim-account-form.tsx +99 -0
  142. package/components/branding/app-logo.test.tsx +20 -0
  143. package/components/branding/app-logo.tsx +25 -0
  144. package/components/calendar/calendar-agenda.tsx +150 -0
  145. package/components/calendar/calendar-drag.test.tsx +177 -0
  146. package/components/calendar/calendar-grid.tsx +357 -0
  147. package/components/calendar/calendar-hooks.test.tsx +27 -0
  148. package/components/calendar/calendar-hooks.ts +351 -0
  149. package/components/calendar/calendar-toolbar.test.tsx +68 -0
  150. package/components/calendar/calendar-toolbar.tsx +291 -0
  151. package/components/calendar/calendar-types.ts +148 -0
  152. package/components/calendar/calendar-view.test.tsx +295 -0
  153. package/components/calendar/calendar-view.tsx +307 -0
  154. package/components/calendar/day-detail-popover.tsx +174 -0
  155. package/components/calendar/task-chip.tsx +86 -0
  156. package/components/command/command-palette.test.tsx +33 -0
  157. package/components/command/command-palette.tsx +310 -0
  158. package/components/download-cta.tsx +87 -0
  159. package/components/feedback/feedback-popup.tsx +207 -0
  160. package/components/graph/graph-draw.ts +337 -0
  161. package/components/graph/graph-overlays.tsx +160 -0
  162. package/components/graph/graph-page.test.tsx +131 -0
  163. package/components/graph/graph-page.tsx +263 -0
  164. package/components/graph/graph-types.ts +47 -0
  165. package/components/graph/graph-view.tsx +322 -0
  166. package/components/guide/guide-view.tsx +522 -0
  167. package/components/kanban/kanban-board.test.tsx +128 -0
  168. package/components/kanban/kanban-board.tsx +361 -0
  169. package/components/kanban/kanban-card-menu.tsx +102 -0
  170. package/components/kanban/kanban-card.tsx +227 -0
  171. package/components/kanban/kanban-column.tsx +49 -0
  172. package/components/kanban/kanban-status-context.tsx +28 -0
  173. package/components/landing/calendar-sandbox.test.tsx +15 -0
  174. package/components/landing/calendar-sandbox.tsx +107 -0
  175. package/components/landing/graph-sandbox.test.tsx +27 -0
  176. package/components/landing/graph-sandbox.tsx +80 -0
  177. package/components/landing/kanban-sandbox.test.tsx +24 -0
  178. package/components/landing/kanban-sandbox.tsx +101 -0
  179. package/components/landing/landing-showcase.test.tsx +21 -0
  180. package/components/landing/landing-showcase.tsx +54 -0
  181. package/components/landing/list-sandbox.tsx +86 -0
  182. package/components/landing/mock-workspace.ts +168 -0
  183. package/components/landing/notes-sandbox.test.tsx +14 -0
  184. package/components/landing/notes-sandbox.tsx +88 -0
  185. package/components/layout/app-shell.tsx +83 -0
  186. package/components/layout/backup-scheduler.tsx +122 -0
  187. package/components/layout/bottom-nav.tsx +43 -0
  188. package/components/layout/icon-bar.test.tsx +29 -0
  189. package/components/layout/icon-bar.tsx +118 -0
  190. package/components/layout/mobile-top-bar.tsx +68 -0
  191. package/components/layout/notes-panel-folder.tsx +127 -0
  192. package/components/layout/notes-panel-note-item.tsx +140 -0
  193. package/components/layout/notes-panel-task-tab.tsx +63 -0
  194. package/components/layout/notes-panel-types.ts +44 -0
  195. package/components/layout/notes-panel.tsx +476 -0
  196. package/components/layout/notification-bell.tsx +251 -0
  197. package/components/layout/paywall-screen.tsx +41 -0
  198. package/components/layout/pro-banner.tsx +76 -0
  199. package/components/layout/sw-register.tsx +27 -0
  200. package/components/layout/workspace-switcher.tsx +90 -0
  201. package/components/notes/mobile-bottom-sheet.tsx +99 -0
  202. package/components/notes/note-editor-context-menu.tsx +47 -0
  203. package/components/notes/note-editor-dom.ts +33 -0
  204. package/components/notes/note-editor-dropdowns.tsx +484 -0
  205. package/components/notes/note-editor-hooks.ts +692 -0
  206. package/components/notes/note-editor-keyboard.ts +305 -0
  207. package/components/notes/note-editor-overlay.tsx +90 -0
  208. package/components/notes/note-editor.test.tsx +372 -0
  209. package/components/notes/note-editor.tsx +662 -0
  210. package/components/notes/note-preview-pane.tsx +156 -0
  211. package/components/notes/note-tabs.tsx +120 -0
  212. package/components/notes/note-types.tsx +157 -0
  213. package/components/settings/accept-invite.tsx +108 -0
  214. package/components/settings/agent-token-settings.tsx +369 -0
  215. package/components/settings/backup-restore-settings.test.tsx +25 -0
  216. package/components/settings/backup-restore-settings.tsx +327 -0
  217. package/components/settings/calendar-feeds-settings.tsx +489 -0
  218. package/components/settings/calendar-general-settings.tsx +174 -0
  219. package/components/settings/confirm-danger-action.test.tsx +215 -0
  220. package/components/settings/confirm-danger-action.tsx +65 -0
  221. package/components/settings/security-settings.tsx +252 -0
  222. package/components/settings/settings-guidance.test.tsx +98 -0
  223. package/components/settings/team-settings.tsx +319 -0
  224. package/components/settings/two-factor-auth.tsx +296 -0
  225. package/components/settings/workspace-settings-client.tsx +363 -0
  226. package/components/settings/workspace-settings-form.tsx +73 -0
  227. package/components/sync/conflict-viewer.tsx +247 -0
  228. package/components/sync/sync-indicator.tsx +171 -0
  229. package/components/tasks/snippet-thread.tsx +119 -0
  230. package/components/tasks/status-dot.tsx +47 -0
  231. package/components/tasks/task-badge.tsx +43 -0
  232. package/components/tasks/task-detail.test.tsx +187 -0
  233. package/components/tasks/task-detail.tsx +458 -0
  234. package/components/tasks/task-list-filters.test.tsx +75 -0
  235. package/components/tasks/task-list-filters.tsx +163 -0
  236. package/components/tasks/task-list-types.ts +20 -0
  237. package/components/tasks/task-list.test.tsx +175 -0
  238. package/components/tasks/task-list.tsx +481 -0
  239. package/components/tasks/task-row.tsx +85 -0
  240. package/components/tasks/task-table-row.tsx +259 -0
  241. package/components/ui/skeleton.tsx +3 -0
  242. package/components/ui/toast.test.tsx +42 -0
  243. package/components/ui/toast.tsx +70 -0
  244. package/instrumentation.tsx +23 -0
  245. package/lib/api-error.ts +50 -0
  246. package/lib/backup/backup-runner.test.ts +32 -0
  247. package/lib/backup/backup-runner.ts +19 -0
  248. package/lib/backup/backup-schedule.test.ts +23 -0
  249. package/lib/backup/backup-schedule.ts +55 -0
  250. package/lib/backup/backup-settings.test.ts +30 -0
  251. package/lib/backup/backup-settings.ts +27 -0
  252. package/lib/backup/export-notes-zip.test.ts +26 -0
  253. package/lib/backup/export-notes-zip.ts +82 -0
  254. package/lib/backup/export-workspace-backup.test.ts +17 -0
  255. package/lib/backup/export-workspace-backup.ts +77 -0
  256. package/lib/backup/restore-workspace-from-export.test.ts +18 -0
  257. package/lib/backup/restore-workspace-from-export.ts +183 -0
  258. package/lib/backup/types.ts +14 -0
  259. package/lib/brand-icons.ts +1 -0
  260. package/lib/calendar-feed-crypto.ts +38 -0
  261. package/lib/calendar-feed.ts +239 -0
  262. package/lib/client/online-status.ts +47 -0
  263. package/lib/conflict-resolver.test.ts +57 -0
  264. package/lib/conflict-resolver.ts +240 -0
  265. package/lib/db-init.ts +79 -0
  266. package/lib/email.ts +159 -0
  267. package/lib/encryption.test.ts +41 -0
  268. package/lib/encryption.ts +98 -0
  269. package/lib/extract-snippet.test.ts +123 -0
  270. package/lib/extract-snippet.ts +69 -0
  271. package/lib/kanban-status.ts +55 -0
  272. package/lib/license.ts +21 -0
  273. package/lib/limits.ts +31 -0
  274. package/lib/mcp-auth.test.ts +58 -0
  275. package/lib/mcp-auth.ts +65 -0
  276. package/lib/mcp-contract.test.ts +25 -0
  277. package/lib/mcp-contract.ts +210 -0
  278. package/lib/mcp-handler.ts +31 -0
  279. package/lib/mcp-url.test.ts +12 -0
  280. package/lib/mcp-url.ts +7 -0
  281. package/lib/mentions.test.ts +45 -0
  282. package/lib/mentions.ts +73 -0
  283. package/lib/note-crypto.ts +108 -0
  284. package/lib/note-sync.ts +201 -0
  285. package/lib/note-title.ts +93 -0
  286. package/lib/prisma.ts +193 -0
  287. package/lib/pro-flush.ts +292 -0
  288. package/lib/rate-limit.ts +57 -0
  289. package/lib/stripe.ts +38 -0
  290. package/lib/sync-worker.ts +388 -0
  291. package/lib/task-parser.test.ts +91 -0
  292. package/lib/task-parser.ts +81 -0
  293. package/lib/task-utils.ts +52 -0
  294. package/lib/use-is-electron.ts +19 -0
  295. package/lib/use-is-mobile.ts +22 -0
  296. package/lib/validation/calendar-feed.ts +31 -0
  297. package/lib/validation/note.ts +27 -0
  298. package/lib/validation/task.ts +26 -0
  299. package/lib/view-preferences.test.ts +54 -0
  300. package/lib/view-preferences.ts +28 -0
  301. package/lib/workspace.ts +66 -0
  302. package/next.config.ts +21 -0
  303. package/package.json +49 -3
  304. package/postcss.config.mjs +7 -0
  305. package/prisma/migrations/20260519021916_init/migration.sql +388 -0
  306. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
  307. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
  308. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
  309. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
  310. package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
  311. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
  312. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
  313. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
  314. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
  315. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
  316. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
  317. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
  318. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
  319. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
  320. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
  321. package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
  322. package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
  323. package/prisma/migrations/migration_lock.toml +3 -0
  324. package/prisma/schema.prisma +457 -0
  325. package/public/Logo_icon.svg +1 -0
  326. package/public/file.svg +1 -0
  327. package/public/globe.svg +1 -0
  328. package/public/icon-192.png +0 -0
  329. package/public/icon-512.png +0 -0
  330. package/public/icon.svg +4 -0
  331. package/public/icon_dark.svg +1 -0
  332. package/public/knotpad_icon.svg +1 -0
  333. package/public/knotpad_logo_full.svg +1 -0
  334. package/public/manifest.json +14 -0
  335. package/public/next.svg +1 -0
  336. package/public/sw.js +137 -0
  337. package/public/vercel.svg +1 -0
  338. package/public/window.svg +1 -0
  339. package/tsconfig.json +35 -0
  340. package/brief.js +0 -311
@@ -0,0 +1,430 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { verifyMcpToken, extractBearerToken } from "@/lib/mcp-auth";
3
+ import { rateLimit, getClientIp } from "@/lib/rate-limit";
4
+ import { prisma } from "@/lib/prisma";
5
+ import type { Priority } from "@/app/generated/prisma/client";
6
+ import { parseTasksFromMarkdown, isAgentHandle } from "@/lib/task-parser";
7
+ import { encryptContent, decryptContent } from "@/lib/note-crypto";
8
+ import { SERVER_INSTRUCTIONS, TOOLS } from "@/lib/mcp-contract";
9
+
10
+ // Sync ((task title)) cross-note references — runs outside the main transaction
11
+ async function syncRefs(noteId: string, workspaceId: string, content: string) {
12
+ const refMap = new Map<string, string>();
13
+ const lines = content.split("\n");
14
+ for (let i = 0; i < lines.length; i++) {
15
+ for (const m of lines[i].matchAll(/\(\(([^)]+)\)\)/g)) {
16
+ const title = m[1].trim();
17
+ if (title && !refMap.has(title)) {
18
+ const start = Math.max(0, i - 1);
19
+ refMap.set(title, lines.slice(start, Math.min(lines.length, i + 2)).join("\n"));
20
+ }
21
+ }
22
+ }
23
+ await prisma.taskReference.deleteMany({ where: { noteId } });
24
+ if (refMap.size === 0) return;
25
+ const matched = await prisma.task.findMany({
26
+ where: { workspaceId, title: { in: Array.from(refMap.keys()) } },
27
+ select: { id: true, title: true },
28
+ });
29
+ if (matched.length > 0) {
30
+ await prisma.taskReference.createMany({
31
+ data: matched.map((t) => ({ taskId: t.id, noteId, snippet: "" })),
32
+ });
33
+ }
34
+ }
35
+
36
+ type McpCtx = { userId: string; workspaceId: string; tokenId: string; alias: string | null };
37
+
38
+ const JSON_HEADERS = { "Content-Type": "application/json" };
39
+
40
+ function toolResult(id: unknown, data: unknown): NextResponse {
41
+ return NextResponse.json(
42
+ { jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] } },
43
+ { headers: JSON_HEADERS }
44
+ );
45
+ }
46
+
47
+ function rpcError(id: unknown, code: number, message: string): NextResponse {
48
+ return NextResponse.json(
49
+ { jsonrpc: "2.0", id, error: { code, message } },
50
+ { headers: JSON_HEADERS }
51
+ );
52
+ }
53
+
54
+ async function callTool(
55
+ name: string,
56
+ args: Record<string, unknown>,
57
+ ctx: McpCtx,
58
+ id: unknown
59
+ ): Promise<NextResponse> {
60
+ const { workspaceId, userId, alias } = ctx;
61
+
62
+ switch (name) {
63
+ case "get_notes": {
64
+ const notes = await prisma.note.findMany({
65
+ where: { workspaceId },
66
+ orderBy: { updatedAt: "desc" },
67
+ select: {
68
+ id: true, title: true, isLocked: true, createdAt: true, updatedAt: true,
69
+ _count: { select: { tasks: true } },
70
+ },
71
+ });
72
+ return toolResult(id, notes.map((n) => ({ ...n, taskCount: n._count.tasks })));
73
+ }
74
+
75
+ case "get_tasks": {
76
+ const tasks = await prisma.task.findMany({
77
+ where: { workspaceId, assigneeType: "AGENT", status: "OPEN", claimedBy: null },
78
+ orderBy: { createdAt: "asc" },
79
+ include: { note: { select: { id: true, title: true } } },
80
+ });
81
+ return toolResult(id, tasks.map((t) => ({
82
+ id: t.id,
83
+ title: t.title,
84
+ fileRefs: t.fileRefs,
85
+ sourceNote: { id: t.note.id, title: t.note.title },
86
+ age: Math.floor((Date.now() - t.createdAt.getTime()) / 1000 / 60) + "m",
87
+ })));
88
+ }
89
+
90
+ case "get_task_detail": {
91
+ const task = await prisma.task.findFirst({
92
+ where: { id: args.taskId as string, workspaceId },
93
+ include: {
94
+ note: { select: { id: true, title: true } },
95
+ auditLogs: { orderBy: { createdAt: "desc" }, take: 10 },
96
+ },
97
+ });
98
+ if (!task) return rpcError(id, -32002, "Task not found");
99
+ return toolResult(id, task);
100
+ }
101
+
102
+ case "get_linked_files": {
103
+ const task = await prisma.task.findFirst({
104
+ where: { id: args.taskId as string, workspaceId },
105
+ select: { fileRefs: true },
106
+ });
107
+ if (!task) return rpcError(id, -32002, "Task not found");
108
+ return toolResult(id, { fileRefs: task.fileRefs });
109
+ }
110
+
111
+ case "claim_tasks": {
112
+ const ids = args.ids as string[];
113
+ if (!Array.isArray(ids) || ids.length === 0) return rpcError(id, -32602, "ids array required");
114
+ const now = new Date();
115
+ const claimedByAlias = alias ?? userId;
116
+ const claimed: string[] = [];
117
+ const failed: string[] = [];
118
+ for (const taskId of ids) {
119
+ const result = await prisma.task.updateMany({
120
+ where: { id: taskId, workspaceId, status: "OPEN", claimedBy: null },
121
+ data: { status: "CLAIMED", claimedBy: userId, claimedByAlias, claimedAt: now, lastHeartbeat: now },
122
+ });
123
+ if (result.count > 0) {
124
+ await prisma.auditLog.create({
125
+ data: { taskId, userId, action: "claimed", detail: `claimed by @agent:${claimedByAlias}` },
126
+ });
127
+ claimed.push(taskId);
128
+ } else {
129
+ failed.push(taskId);
130
+ }
131
+ }
132
+ return toolResult(id, { claimed, failed });
133
+ }
134
+
135
+ case "release_task": {
136
+ const taskId = args.id as string;
137
+ const reason = args.reason as string | undefined;
138
+ const task = await prisma.task.findFirst({ where: { id: taskId, workspaceId, claimedBy: userId } });
139
+ if (!task) return rpcError(id, -32002, "Task not found or not claimed by you");
140
+ await prisma.$transaction([
141
+ prisma.task.update({
142
+ where: { id: taskId },
143
+ data: { status: "OPEN", claimedBy: null, claimedByAlias: null, claimedAt: null, lastHeartbeat: null },
144
+ }),
145
+ prisma.auditLog.create({
146
+ data: {
147
+ taskId,
148
+ userId,
149
+ action: "released",
150
+ detail: reason
151
+ ? `released by @agent:${alias ?? userId}: ${reason}`
152
+ : `released by @agent:${alias ?? userId}`,
153
+ },
154
+ }),
155
+ ]);
156
+ const owner = await prisma.workspaceMember.findFirst({ where: { workspaceId, role: "OWNER" } });
157
+ if (owner) {
158
+ await prisma.notification.create({
159
+ data: {
160
+ userId: owner.userId,
161
+ type: "task_released",
162
+ title: `Task released: "${task.title}"`,
163
+ body: reason ?? undefined,
164
+ taskId,
165
+ },
166
+ });
167
+ }
168
+ return toolResult(id, { ok: true });
169
+ }
170
+
171
+ case "update_task": {
172
+ const STATUS_MAP: Record<string, "IN_PROGRESS" | "REVIEW" | "DONE"> = {
173
+ in_progress: "IN_PROGRESS", review: "REVIEW", done: "DONE",
174
+ };
175
+ const status = args.status as string;
176
+ if (!STATUS_MAP[status]) return rpcError(id, -32602, "status must be one of: in_progress, review, done");
177
+ const taskId = args.id as string;
178
+ const task = await prisma.task.findFirst({ where: { id: taskId, workspaceId, claimedBy: userId } });
179
+ if (!task) return rpcError(id, -32002, "Task not found or not claimed by you");
180
+ const newStatus = STATUS_MAP[status];
181
+ await prisma.$transaction(async (tx) => {
182
+ await tx.task.update({
183
+ where: { id: taskId },
184
+ data: { status: newStatus, lastHeartbeat: new Date(), version: { increment: 1 } },
185
+ });
186
+ await tx.auditLog.create({
187
+ data: { taskId, userId, action: "status_change", detail: `${task.status} → ${newStatus} by @agent:${alias ?? userId}` },
188
+ });
189
+ if (newStatus === "DONE") {
190
+ const note = await tx.note.findUnique({ where: { id: task.noteId } });
191
+ if (note) {
192
+ const plain = await decryptContent(note.content, workspaceId);
193
+ const lines = plain.split("\n");
194
+ let changed = false;
195
+ for (let i = 0; i < lines.length; i++) {
196
+ if (lines[i].includes("[ ]") && lines[i].includes(task.title)) {
197
+ lines[i] = lines[i].replace("[ ]", "[x]").replace(/\s*<!--task::[A-Z_]+-->/, "") + " <!--task::DONE-->";
198
+ changed = true;
199
+ break;
200
+ }
201
+ }
202
+ if (changed) {
203
+ const stored = await encryptContent(lines.join("\n"), workspaceId);
204
+ await tx.note.update({ where: { id: task.noteId }, data: { content: stored, version: { increment: 1 } } });
205
+ }
206
+ }
207
+ }
208
+ });
209
+ return toolResult(id, { ok: true, status: newStatus });
210
+ }
211
+
212
+ case "heartbeat": {
213
+ const result = await prisma.task.updateMany({
214
+ where: { id: args.id as string, workspaceId, claimedBy: userId },
215
+ data: { lastHeartbeat: new Date() },
216
+ });
217
+ if (result.count === 0) return rpcError(id, -32002, "Task not found or not claimed by you");
218
+ return toolResult(id, { ok: true });
219
+ }
220
+
221
+ case "create_note": {
222
+ const title = args.title as string;
223
+ const plainContent = (args.content as string | undefined) ?? "";
224
+ const storedContent = await encryptContent(plainContent, workspaceId);
225
+ const note = await prisma.$transaction(async (tx) => {
226
+ const newNote = await tx.note.create({ data: { title, content: storedContent, workspaceId } });
227
+ const taskIds: string[] = [];
228
+ if (plainContent) {
229
+ for (const p of parseTasksFromMarkdown(plainContent)) {
230
+ const assigneeType = p.assigneeHandle && isAgentHandle(p.assigneeHandle) ? "AGENT" : "HUMAN";
231
+ const t = await tx.task.create({
232
+ data: {
233
+ title: p.title, noteId: newNote.id, workspaceId, assigneeType, fileRefs: p.fileRefs,
234
+ ...(p.isChecked && { status: "DONE" as const }),
235
+ ...(p.startDate && { startDate: new Date(p.startDate) }),
236
+ ...(p.dueDate && { dueDate: new Date(p.dueDate) }),
237
+ ...(p.priority && { priority: p.priority as Priority }),
238
+ },
239
+ });
240
+ taskIds.push(t.id);
241
+ }
242
+ }
243
+ return { id: newNote.id, taskIds };
244
+ });
245
+ // syncRefs runs async — don't await so it never blocks or 500s the response
246
+ if (plainContent) syncRefs(note.id, workspaceId, plainContent).catch(() => {});
247
+ return toolResult(id, note);
248
+ }
249
+
250
+ case "get_note": {
251
+ const note = await prisma.note.findFirst({
252
+ where: { id: args.noteId as string, workspaceId },
253
+ include: {
254
+ tasks: { select: { id: true, title: true, status: true, assigneeType: true, claimedByAlias: true } },
255
+ },
256
+ });
257
+ if (!note) return rpcError(id, -32002, "Note not found");
258
+ const content = await decryptContent(note.content, workspaceId);
259
+ return toolResult(id, { ...note, content });
260
+ }
261
+
262
+ case "append_to_note": {
263
+ const noteId = args.noteId as string;
264
+ const appendContent = args.content as string;
265
+ const note = await prisma.note.findFirst({ where: { id: noteId, workspaceId } });
266
+ if (!note) return rpcError(id, -32002, "Note not found");
267
+ if (note.isLocked) return rpcError(id, -32003, "Note is locked");
268
+ const existingPlain = await decryptContent(note.content, workspaceId);
269
+ const newPlainContent = existingPlain + "\n" + appendContent;
270
+ const storedContent = await encryptContent(newPlainContent, workspaceId);
271
+ const result = await prisma.$transaction(async (tx) => {
272
+ await tx.note.update({ where: { id: noteId }, data: { content: storedContent, version: { increment: 1 } } });
273
+ const existingTasks = await tx.task.findMany({ where: { noteId }, select: { title: true } });
274
+ const existingTitles = new Set(existingTasks.map((t) => t.title));
275
+ const newTasks: string[] = [];
276
+ for (const p of parseTasksFromMarkdown(newPlainContent)) {
277
+ if (!existingTitles.has(p.title)) {
278
+ const assigneeType = p.assigneeHandle && isAgentHandle(p.assigneeHandle) ? "AGENT" : "HUMAN";
279
+ const t = await tx.task.create({
280
+ data: {
281
+ title: p.title, noteId, workspaceId, assigneeType, fileRefs: p.fileRefs,
282
+ ...(p.isChecked && { status: "DONE" as const }),
283
+ ...(p.startDate && { startDate: new Date(p.startDate) }),
284
+ ...(p.dueDate && { dueDate: new Date(p.dueDate) }),
285
+ ...(p.priority && { priority: p.priority as Priority }),
286
+ },
287
+ });
288
+ newTasks.push(t.id);
289
+ }
290
+ }
291
+ return { newTaskIds: newTasks };
292
+ });
293
+ syncRefs(noteId, workspaceId, newPlainContent).catch(() => {});
294
+ return toolResult(id, result);
295
+ }
296
+
297
+ default:
298
+ return rpcError(id, -32601, `Unknown tool: ${name}`);
299
+ }
300
+ }
301
+
302
+ // SSE stream for server→client notifications (required by MCP Streamable HTTP spec)
303
+ export async function GET(req: NextRequest): Promise<Response> {
304
+ const token = extractBearerToken(req.headers.get("authorization"));
305
+ if (!token) {
306
+ return new Response(JSON.stringify({ error: "Missing authorization token" }), {
307
+ status: 401,
308
+ headers: { "Content-Type": "application/json" },
309
+ });
310
+ }
311
+
312
+ const ctx = await verifyMcpToken(token);
313
+ if (!ctx) {
314
+ return new Response(JSON.stringify({ error: "Invalid or revoked token" }), {
315
+ status: 401,
316
+ headers: { "Content-Type": "application/json" },
317
+ });
318
+ }
319
+
320
+ // Open an SSE stream that stays alive for server-push notifications
321
+ const controller = new AbortController();
322
+ const stream = new ReadableStream({
323
+ start(ctrl) {
324
+ const encoder = new TextEncoder();
325
+ // Send an initial keep-alive comment so the client knows the stream is open
326
+ ctrl.enqueue(encoder.encode(": stream opened\n\n"));
327
+
328
+ // Send periodic keep-alive comments to prevent timeout
329
+ const interval = setInterval(() => {
330
+ try {
331
+ ctrl.enqueue(encoder.encode(": keepalive\n\n"));
332
+ } catch {
333
+ clearInterval(interval);
334
+ }
335
+ }, 30_000);
336
+
337
+ // Clean up on client disconnect or server abort
338
+ const cleanup = () => {
339
+ clearInterval(interval);
340
+ try { ctrl.close(); } catch {}
341
+ };
342
+ controller.signal.addEventListener("abort", cleanup);
343
+ // Also listen for the request abort (client disconnect)
344
+ if (req.signal) {
345
+ req.signal.addEventListener("abort", () => {
346
+ controller.abort();
347
+ });
348
+ }
349
+ },
350
+ });
351
+
352
+ return new Response(stream, {
353
+ headers: {
354
+ "Content-Type": "text/event-stream",
355
+ "Cache-Control": "no-cache",
356
+ Connection: "keep-alive",
357
+ },
358
+ });
359
+ }
360
+
361
+ export async function POST(req: NextRequest): Promise<NextResponse> {
362
+ const ip = getClientIp(req);
363
+ const rl = rateLimit(`mcp:${ip}`, 120, 60_000);
364
+ if (rl.limited) {
365
+ return NextResponse.json(
366
+ { jsonrpc: "2.0", id: null, error: { code: -32029, message: "Too many requests" } },
367
+ { status: 429, headers: { ...JSON_HEADERS, "Retry-After": String(rl.retryAfter) } }
368
+ );
369
+ }
370
+
371
+ const token = extractBearerToken(req.headers.get("authorization"));
372
+ if (!token) return rpcError(null, -32001, "Missing authorization token");
373
+
374
+ const ctx = await verifyMcpToken(token);
375
+ if (!ctx) return rpcError(null, -32001, "Invalid or revoked token");
376
+
377
+ let body: { id?: unknown; method?: string; params?: unknown };
378
+ try {
379
+ body = await req.json();
380
+ } catch {
381
+ return rpcError(null, -32700, "Parse error");
382
+ }
383
+
384
+ const { id = null, method, params } = body;
385
+
386
+ if (method === "initialize") {
387
+ return NextResponse.json(
388
+ {
389
+ jsonrpc: "2.0",
390
+ id,
391
+ result: {
392
+ protocolVersion: "2024-11-05",
393
+ capabilities: { tools: {} },
394
+ serverInfo: { name: "knotpad", version: "1.0.0" },
395
+ instructions: SERVER_INSTRUCTIONS,
396
+ },
397
+ },
398
+ { headers: JSON_HEADERS }
399
+ );
400
+ }
401
+
402
+ // ping — MCP clients use this to verify the server is alive
403
+ if (method === "ping") {
404
+ return NextResponse.json(
405
+ { jsonrpc: "2.0", id, result: {} },
406
+ { headers: JSON_HEADERS }
407
+ );
408
+ }
409
+
410
+ if (method === "notifications/initialized") {
411
+ return new NextResponse(null, { status: 204 });
412
+ }
413
+
414
+ if (method === "tools/list") {
415
+ return NextResponse.json(
416
+ { jsonrpc: "2.0", id, result: { tools: TOOLS } },
417
+ { headers: JSON_HEADERS }
418
+ );
419
+ }
420
+
421
+ if (method === "tools/call") {
422
+ const p = (params ?? {}) as { name?: string; arguments?: Record<string, unknown> };
423
+ const name = p.name ?? "";
424
+ const args = p.arguments ?? {};
425
+ if (!name) return rpcError(id, -32602, "Missing tool name in params");
426
+ return callTool(name, args, ctx, id);
427
+ }
428
+
429
+ return rpcError(id, -32601, "Method not found");
430
+ }
@@ -0,0 +1,120 @@
1
+ import { ImageResponse } from "next/og";
2
+
3
+ // Image metadata — Next.js serves this at /opengraph-image.png
4
+ export const runtime = "edge";
5
+ export const alt = "Knotpad — Write the note. The tasks come with it.";
6
+ export const size = { width: 1200, height: 630 };
7
+ export const contentType = "image/png";
8
+
9
+ export default function OgImage() {
10
+ return new ImageResponse(
11
+ (
12
+ <div
13
+ style={{
14
+ width: "100%",
15
+ height: "100%",
16
+ display: "flex",
17
+ flexDirection: "column",
18
+ justifyContent: "center",
19
+ backgroundColor: "#09090b",
20
+ padding: "80px 100px",
21
+ fontFamily: "sans-serif",
22
+ }}
23
+ >
24
+ {/* Logo mark */}
25
+ <div
26
+ style={{
27
+ display: "flex",
28
+ alignItems: "center",
29
+ gap: "16px",
30
+ marginBottom: "40px",
31
+ }}
32
+ >
33
+ <div
34
+ style={{
35
+ width: "56px",
36
+ height: "56px",
37
+ borderRadius: "12px",
38
+ backgroundColor: "#18181b",
39
+ border: "1px solid #27272a",
40
+ display: "flex",
41
+ alignItems: "center",
42
+ justifyContent: "center",
43
+ fontSize: "28px",
44
+ fontWeight: 700,
45
+ color: "#fafafa",
46
+ }}
47
+ >
48
+ K
49
+ </div>
50
+ <span
51
+ style={{
52
+ fontSize: "24px",
53
+ fontWeight: 600,
54
+ color: "#a1a1aa",
55
+ letterSpacing: "-0.02em",
56
+ }}
57
+ >
58
+ knotpad.app
59
+ </span>
60
+ </div>
61
+
62
+ {/* Headline */}
63
+ <h1
64
+ style={{
65
+ fontSize: "72px",
66
+ fontWeight: 700,
67
+ color: "#fafafa",
68
+ lineHeight: 1.1,
69
+ letterSpacing: "-0.03em",
70
+ margin: 0,
71
+ }}
72
+ >
73
+ Write the note.
74
+ <br />
75
+ <span style={{ color: "#52525b" }}>The tasks come with it.</span>
76
+ </h1>
77
+
78
+ {/* Tagline */}
79
+ <p
80
+ style={{
81
+ fontSize: "28px",
82
+ color: "#71717a",
83
+ marginTop: "32px",
84
+ lineHeight: 1.4,
85
+ }}
86
+ >
87
+ Note-first project management with AI agent task routing
88
+ </p>
89
+
90
+ {/* Bottom badges */}
91
+ <div
92
+ style={{
93
+ display: "flex",
94
+ gap: "12px",
95
+ marginTop: "auto",
96
+ }}
97
+ >
98
+ {["Notes → Tasks", "Kanban", "Calendar", "AI Agents", "Local-first"].map(
99
+ (tag) => (
100
+ <span
101
+ key={tag}
102
+ style={{
103
+ fontSize: "16px",
104
+ color: "#a1a1aa",
105
+ backgroundColor: "#18181b",
106
+ border: "1px solid #27272a",
107
+ borderRadius: "9999px",
108
+ padding: "6px 16px",
109
+ }}
110
+ >
111
+ {tag}
112
+ </span>
113
+ )
114
+ )}
115
+ </div>
116
+ </div>
117
+ ),
118
+ { ...size }
119
+ );
120
+ }