@knotpad/app 0.1.0 → 0.1.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 (341) hide show
  1. package/bin/brief.js +165 -78
  2. package/package.json +3 -17
  3. package/app/(app)/calendar/page.tsx +0 -57
  4. package/app/(app)/error.tsx +0 -35
  5. package/app/(app)/graph/page.tsx +0 -32
  6. package/app/(app)/guide/page.tsx +0 -21
  7. package/app/(app)/kanban/loading.tsx +0 -24
  8. package/app/(app)/kanban/page.tsx +0 -59
  9. package/app/(app)/layout.tsx +0 -122
  10. package/app/(app)/list/loading.tsx +0 -21
  11. package/app/(app)/list/page.tsx +0 -137
  12. package/app/(app)/loading.tsx +0 -18
  13. package/app/(app)/notes/[noteId]/page.tsx +0 -84
  14. package/app/(app)/notes/layout.tsx +0 -30
  15. package/app/(app)/notes/page.tsx +0 -39
  16. package/app/(app)/page.tsx +0 -5
  17. package/app/(app)/settings/agent-token/page.tsx +0 -59
  18. package/app/(app)/settings/backup/page.tsx +0 -49
  19. package/app/(app)/settings/billing/page.tsx +0 -53
  20. package/app/(app)/settings/calendar/page.tsx +0 -41
  21. package/app/(app)/settings/layout.test.tsx +0 -39
  22. package/app/(app)/settings/layout.tsx +0 -71
  23. package/app/(app)/settings/page.tsx +0 -4
  24. package/app/(app)/settings/security/page.tsx +0 -43
  25. package/app/(app)/settings/team/page.tsx +0 -74
  26. package/app/(app)/settings/workspace/page.tsx +0 -27
  27. package/app/(app)/tasks/[taskId]/page.tsx +0 -79
  28. package/app/(auth)/forgot-password/page.tsx +0 -106
  29. package/app/(auth)/guest/page.tsx +0 -56
  30. package/app/(auth)/layout.tsx +0 -13
  31. package/app/(auth)/login/page.tsx +0 -14
  32. package/app/(auth)/register/page.tsx +0 -193
  33. package/app/(auth)/reset-password/page.tsx +0 -138
  34. package/app/api/account/claim/route.tsx +0 -135
  35. package/app/api/admin/backfill-encryption/route.tsx +0 -43
  36. package/app/api/admin/license/route.tsx +0 -42
  37. package/app/api/auth/2fa/route.tsx +0 -148
  38. package/app/api/auth/[...nextauth]/route.tsx +0 -3
  39. package/app/api/auth/change-password/route.tsx +0 -61
  40. package/app/api/auth/check-2fa/route.tsx +0 -19
  41. package/app/api/auth/forgot-password/route.tsx +0 -65
  42. package/app/api/auth/reset-password/route.tsx +0 -52
  43. package/app/api/auth/verify-2fa/route.tsx +0 -88
  44. package/app/api/backup/download/db/route.ts +0 -29
  45. package/app/api/backup/download/notes/route.ts +0 -25
  46. package/app/api/backup/settings/route.ts +0 -92
  47. package/app/api/billing/checkout/route.tsx +0 -81
  48. package/app/api/billing/migrate/route.tsx +0 -163
  49. package/app/api/billing/portal/route.tsx +0 -24
  50. package/app/api/billing/setup-intent/route.tsx +0 -55
  51. package/app/api/billing/status/route.tsx +0 -36
  52. package/app/api/billing/subscribe/route.tsx +0 -85
  53. package/app/api/billing/webhook/route.tsx +0 -199
  54. package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
  55. package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
  56. package/app/api/calendar-feeds/events/route.tsx +0 -82
  57. package/app/api/calendar-feeds/route.tsx +0 -52
  58. package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
  59. package/app/api/cron/calendar-feeds/route.tsx +0 -31
  60. package/app/api/cron/stale-tasks/route.tsx +0 -51
  61. package/app/api/cron/sync/route.tsx +0 -34
  62. package/app/api/devices/[deviceId]/route.tsx +0 -25
  63. package/app/api/devices/route.tsx +0 -41
  64. package/app/api/export/route.tsx +0 -40
  65. package/app/api/feedback/route.tsx +0 -54
  66. package/app/api/folders/[folderId]/route.tsx +0 -51
  67. package/app/api/folders/route.tsx +0 -37
  68. package/app/api/graph/route.tsx +0 -242
  69. package/app/api/guest/route.tsx +0 -58
  70. package/app/api/health/route.tsx +0 -10
  71. package/app/api/holidays/countries/route.tsx +0 -14
  72. package/app/api/holidays/route.tsx +0 -49
  73. package/app/api/holidays/states/route.tsx +0 -21
  74. package/app/api/invites/[token]/route.tsx +0 -131
  75. package/app/api/invites/route.tsx +0 -74
  76. package/app/api/mcp/generate-token/route.tsx +0 -55
  77. package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
  78. package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
  79. package/app/api/notes/[noteId]/export/route.tsx +0 -45
  80. package/app/api/notes/[noteId]/route.tsx +0 -360
  81. package/app/api/notes/route.tsx +0 -112
  82. package/app/api/notifications/route.tsx +0 -44
  83. package/app/api/register/route.tsx +0 -67
  84. package/app/api/restore/route.tsx +0 -148
  85. package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
  86. package/app/api/sync/conflicts/route.tsx +0 -48
  87. package/app/api/sync/status/route.tsx +0 -49
  88. package/app/api/sync/trigger/route.tsx +0 -15
  89. package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
  90. package/app/api/tasks/[taskId]/route.tsx +0 -259
  91. package/app/api/tasks/bulk/route.tsx +0 -133
  92. package/app/api/tasks/route.tsx +0 -36
  93. package/app/api/workspace/active/route.tsx +0 -39
  94. package/app/api/workspace/create-team/route.tsx +0 -42
  95. package/app/api/workspace/kanban-statuses/route.tsx +0 -71
  96. package/app/api/workspace/members/[memberId]/route.tsx +0 -69
  97. package/app/api/workspace/route.tsx +0 -24
  98. package/app/download/page.tsx +0 -170
  99. package/app/favicon.ico +0 -0
  100. package/app/generated/prisma/client.d.ts +0 -1
  101. package/app/generated/prisma/client.js +0 -5
  102. package/app/generated/prisma/default.d.ts +0 -1
  103. package/app/generated/prisma/default.js +0 -5
  104. package/app/generated/prisma/edge.d.ts +0 -1
  105. package/app/generated/prisma/edge.js +0 -497
  106. package/app/generated/prisma/index-browser.js +0 -523
  107. package/app/generated/prisma/index.d.ts +0 -46376
  108. package/app/generated/prisma/index.js +0 -497
  109. package/app/generated/prisma/package.json +0 -144
  110. package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
  111. package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
  112. package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
  113. package/app/generated/prisma/runtime/client.d.ts +0 -3386
  114. package/app/generated/prisma/runtime/client.js +0 -86
  115. package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
  116. package/app/generated/prisma/runtime/index-browser.js +0 -6
  117. package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
  118. package/app/generated/prisma/schema.prisma +0 -456
  119. package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
  120. package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
  121. package/app/globals.css +0 -54
  122. package/app/invite/[token]/page.tsx +0 -52
  123. package/app/layout.tsx +0 -90
  124. package/app/mcp/route.tsx +0 -430
  125. package/app/opengraph-image.tsx +0 -120
  126. package/app/page.tsx +0 -398
  127. package/app/privacy/page.tsx +0 -69
  128. package/app/robots.tsx +0 -25
  129. package/app/sitemap.tsx +0 -36
  130. package/app/terms/page.tsx +0 -69
  131. package/app/upgrade/page.tsx +0 -75
  132. package/auth.config.ts +0 -33
  133. package/auth.ts +0 -79
  134. package/components/auth/login-form.tsx +0 -302
  135. package/components/auth/password-checklist.tsx +0 -31
  136. package/components/auth/password-input.tsx +0 -36
  137. package/components/auth/switch-account-button.test.tsx +0 -22
  138. package/components/auth/switch-account-button.tsx +0 -19
  139. package/components/auth/two-factor-input.tsx +0 -116
  140. package/components/billing/billing-dashboard.tsx +0 -265
  141. package/components/billing/card-form.tsx +0 -210
  142. package/components/billing/claim-account-form.tsx +0 -99
  143. package/components/branding/app-logo.test.tsx +0 -20
  144. package/components/branding/app-logo.tsx +0 -25
  145. package/components/calendar/calendar-agenda.tsx +0 -150
  146. package/components/calendar/calendar-drag.test.tsx +0 -177
  147. package/components/calendar/calendar-grid.tsx +0 -357
  148. package/components/calendar/calendar-hooks.test.tsx +0 -27
  149. package/components/calendar/calendar-hooks.ts +0 -351
  150. package/components/calendar/calendar-toolbar.test.tsx +0 -68
  151. package/components/calendar/calendar-toolbar.tsx +0 -291
  152. package/components/calendar/calendar-types.ts +0 -148
  153. package/components/calendar/calendar-view.test.tsx +0 -295
  154. package/components/calendar/calendar-view.tsx +0 -307
  155. package/components/calendar/day-detail-popover.tsx +0 -174
  156. package/components/calendar/task-chip.tsx +0 -86
  157. package/components/command/command-palette.test.tsx +0 -33
  158. package/components/command/command-palette.tsx +0 -310
  159. package/components/download-cta.tsx +0 -87
  160. package/components/feedback/feedback-popup.tsx +0 -207
  161. package/components/graph/graph-draw.ts +0 -337
  162. package/components/graph/graph-overlays.tsx +0 -160
  163. package/components/graph/graph-page.test.tsx +0 -131
  164. package/components/graph/graph-page.tsx +0 -263
  165. package/components/graph/graph-types.ts +0 -47
  166. package/components/graph/graph-view.tsx +0 -322
  167. package/components/guide/guide-view.tsx +0 -522
  168. package/components/kanban/kanban-board.test.tsx +0 -128
  169. package/components/kanban/kanban-board.tsx +0 -361
  170. package/components/kanban/kanban-card-menu.tsx +0 -102
  171. package/components/kanban/kanban-card.tsx +0 -227
  172. package/components/kanban/kanban-column.tsx +0 -49
  173. package/components/kanban/kanban-status-context.tsx +0 -28
  174. package/components/landing/calendar-sandbox.test.tsx +0 -15
  175. package/components/landing/calendar-sandbox.tsx +0 -107
  176. package/components/landing/graph-sandbox.test.tsx +0 -27
  177. package/components/landing/graph-sandbox.tsx +0 -80
  178. package/components/landing/kanban-sandbox.test.tsx +0 -24
  179. package/components/landing/kanban-sandbox.tsx +0 -101
  180. package/components/landing/landing-showcase.test.tsx +0 -21
  181. package/components/landing/landing-showcase.tsx +0 -54
  182. package/components/landing/list-sandbox.tsx +0 -86
  183. package/components/landing/mock-workspace.ts +0 -168
  184. package/components/landing/notes-sandbox.test.tsx +0 -14
  185. package/components/landing/notes-sandbox.tsx +0 -88
  186. package/components/layout/app-shell.tsx +0 -83
  187. package/components/layout/backup-scheduler.tsx +0 -122
  188. package/components/layout/bottom-nav.tsx +0 -43
  189. package/components/layout/icon-bar.test.tsx +0 -29
  190. package/components/layout/icon-bar.tsx +0 -118
  191. package/components/layout/mobile-top-bar.tsx +0 -68
  192. package/components/layout/notes-panel-folder.tsx +0 -127
  193. package/components/layout/notes-panel-note-item.tsx +0 -140
  194. package/components/layout/notes-panel-task-tab.tsx +0 -63
  195. package/components/layout/notes-panel-types.ts +0 -44
  196. package/components/layout/notes-panel.tsx +0 -476
  197. package/components/layout/notification-bell.tsx +0 -251
  198. package/components/layout/paywall-screen.tsx +0 -41
  199. package/components/layout/pro-banner.tsx +0 -76
  200. package/components/layout/sw-register.tsx +0 -27
  201. package/components/layout/workspace-switcher.tsx +0 -90
  202. package/components/notes/mobile-bottom-sheet.tsx +0 -99
  203. package/components/notes/note-editor-context-menu.tsx +0 -47
  204. package/components/notes/note-editor-dom.ts +0 -33
  205. package/components/notes/note-editor-dropdowns.tsx +0 -484
  206. package/components/notes/note-editor-hooks.ts +0 -692
  207. package/components/notes/note-editor-keyboard.ts +0 -305
  208. package/components/notes/note-editor-overlay.tsx +0 -90
  209. package/components/notes/note-editor.test.tsx +0 -372
  210. package/components/notes/note-editor.tsx +0 -662
  211. package/components/notes/note-preview-pane.tsx +0 -156
  212. package/components/notes/note-tabs.tsx +0 -120
  213. package/components/notes/note-types.tsx +0 -157
  214. package/components/settings/accept-invite.tsx +0 -108
  215. package/components/settings/agent-token-settings.tsx +0 -369
  216. package/components/settings/backup-restore-settings.test.tsx +0 -25
  217. package/components/settings/backup-restore-settings.tsx +0 -327
  218. package/components/settings/calendar-feeds-settings.tsx +0 -489
  219. package/components/settings/calendar-general-settings.tsx +0 -174
  220. package/components/settings/confirm-danger-action.test.tsx +0 -215
  221. package/components/settings/confirm-danger-action.tsx +0 -65
  222. package/components/settings/security-settings.tsx +0 -252
  223. package/components/settings/settings-guidance.test.tsx +0 -98
  224. package/components/settings/team-settings.tsx +0 -319
  225. package/components/settings/two-factor-auth.tsx +0 -296
  226. package/components/settings/workspace-settings-client.tsx +0 -363
  227. package/components/settings/workspace-settings-form.tsx +0 -73
  228. package/components/sync/conflict-viewer.tsx +0 -247
  229. package/components/sync/sync-indicator.tsx +0 -171
  230. package/components/tasks/snippet-thread.tsx +0 -119
  231. package/components/tasks/status-dot.tsx +0 -47
  232. package/components/tasks/task-badge.tsx +0 -43
  233. package/components/tasks/task-detail.test.tsx +0 -187
  234. package/components/tasks/task-detail.tsx +0 -458
  235. package/components/tasks/task-list-filters.test.tsx +0 -75
  236. package/components/tasks/task-list-filters.tsx +0 -163
  237. package/components/tasks/task-list-types.ts +0 -20
  238. package/components/tasks/task-list.test.tsx +0 -175
  239. package/components/tasks/task-list.tsx +0 -481
  240. package/components/tasks/task-row.tsx +0 -85
  241. package/components/tasks/task-table-row.tsx +0 -259
  242. package/components/ui/skeleton.tsx +0 -3
  243. package/components/ui/toast.test.tsx +0 -42
  244. package/components/ui/toast.tsx +0 -70
  245. package/electron/main.ts +0 -251
  246. package/electron/preload.ts +0 -56
  247. package/instrumentation.tsx +0 -23
  248. package/lib/api-error.ts +0 -50
  249. package/lib/backup/backup-runner.test.ts +0 -32
  250. package/lib/backup/backup-runner.ts +0 -19
  251. package/lib/backup/backup-schedule.test.ts +0 -23
  252. package/lib/backup/backup-schedule.ts +0 -55
  253. package/lib/backup/backup-settings.test.ts +0 -30
  254. package/lib/backup/backup-settings.ts +0 -27
  255. package/lib/backup/export-notes-zip.test.ts +0 -26
  256. package/lib/backup/export-notes-zip.ts +0 -82
  257. package/lib/backup/export-workspace-backup.test.ts +0 -17
  258. package/lib/backup/export-workspace-backup.ts +0 -77
  259. package/lib/backup/restore-workspace-from-export.test.ts +0 -18
  260. package/lib/backup/restore-workspace-from-export.ts +0 -183
  261. package/lib/backup/types.ts +0 -14
  262. package/lib/brand-icons.ts +0 -1
  263. package/lib/calendar-feed-crypto.ts +0 -38
  264. package/lib/calendar-feed.ts +0 -239
  265. package/lib/client/online-status.ts +0 -47
  266. package/lib/conflict-resolver.test.ts +0 -57
  267. package/lib/conflict-resolver.ts +0 -240
  268. package/lib/db-init.ts +0 -79
  269. package/lib/email.ts +0 -159
  270. package/lib/encryption.test.ts +0 -41
  271. package/lib/encryption.ts +0 -98
  272. package/lib/extract-snippet.test.ts +0 -123
  273. package/lib/extract-snippet.ts +0 -69
  274. package/lib/kanban-status.ts +0 -55
  275. package/lib/license.ts +0 -21
  276. package/lib/limits.ts +0 -31
  277. package/lib/mcp-auth.test.ts +0 -58
  278. package/lib/mcp-auth.ts +0 -65
  279. package/lib/mcp-contract.test.ts +0 -25
  280. package/lib/mcp-contract.ts +0 -210
  281. package/lib/mcp-handler.ts +0 -31
  282. package/lib/mcp-url.test.ts +0 -12
  283. package/lib/mcp-url.ts +0 -7
  284. package/lib/mentions.test.ts +0 -45
  285. package/lib/mentions.ts +0 -73
  286. package/lib/note-crypto.ts +0 -108
  287. package/lib/note-sync.ts +0 -201
  288. package/lib/note-title.ts +0 -93
  289. package/lib/prisma.ts +0 -193
  290. package/lib/pro-flush.ts +0 -292
  291. package/lib/rate-limit.ts +0 -57
  292. package/lib/stripe.ts +0 -38
  293. package/lib/sync-worker.ts +0 -388
  294. package/lib/task-parser.test.ts +0 -91
  295. package/lib/task-parser.ts +0 -81
  296. package/lib/task-utils.ts +0 -52
  297. package/lib/use-is-electron.ts +0 -19
  298. package/lib/use-is-mobile.ts +0 -22
  299. package/lib/validation/calendar-feed.ts +0 -31
  300. package/lib/validation/note.ts +0 -27
  301. package/lib/validation/task.ts +0 -26
  302. package/lib/view-preferences.test.ts +0 -54
  303. package/lib/view-preferences.ts +0 -28
  304. package/lib/workspace.ts +0 -66
  305. package/next.config.ts +0 -21
  306. package/postcss.config.mjs +0 -7
  307. package/prisma/migrations/20260519021916_init/migration.sql +0 -388
  308. package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
  309. package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
  310. package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
  311. package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
  312. package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
  313. package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
  314. package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
  315. package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
  316. package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
  317. package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
  318. package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
  319. package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
  320. package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
  321. package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
  322. package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
  323. package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
  324. package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
  325. package/prisma/migrations/migration_lock.toml +0 -3
  326. package/prisma/schema.prisma +0 -457
  327. package/public/Logo_icon.svg +0 -1
  328. package/public/file.svg +0 -1
  329. package/public/globe.svg +0 -1
  330. package/public/icon-192.png +0 -0
  331. package/public/icon-512.png +0 -0
  332. package/public/icon.svg +0 -4
  333. package/public/icon_dark.svg +0 -1
  334. package/public/knotpad_icon.svg +0 -1
  335. package/public/knotpad_logo_full.svg +0 -1
  336. package/public/manifest.json +0 -14
  337. package/public/next.svg +0 -1
  338. package/public/sw.js +0 -137
  339. package/public/vercel.svg +0 -1
  340. package/public/window.svg +0 -1
  341. package/tsconfig.json +0 -35
package/app/mcp/route.tsx DELETED
@@ -1,430 +0,0 @@
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
- }
@@ -1,120 +0,0 @@
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
- }