@knotpad/app 0.1.0 → 0.1.2
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/bin/brief.js +165 -78
- package/package.json +6 -86
- package/app/(app)/calendar/page.tsx +0 -57
- package/app/(app)/error.tsx +0 -35
- package/app/(app)/graph/page.tsx +0 -32
- package/app/(app)/guide/page.tsx +0 -21
- package/app/(app)/kanban/loading.tsx +0 -24
- package/app/(app)/kanban/page.tsx +0 -59
- package/app/(app)/layout.tsx +0 -122
- package/app/(app)/list/loading.tsx +0 -21
- package/app/(app)/list/page.tsx +0 -137
- package/app/(app)/loading.tsx +0 -18
- package/app/(app)/notes/[noteId]/page.tsx +0 -84
- package/app/(app)/notes/layout.tsx +0 -30
- package/app/(app)/notes/page.tsx +0 -39
- package/app/(app)/page.tsx +0 -5
- package/app/(app)/settings/agent-token/page.tsx +0 -59
- package/app/(app)/settings/backup/page.tsx +0 -49
- package/app/(app)/settings/billing/page.tsx +0 -53
- package/app/(app)/settings/calendar/page.tsx +0 -41
- package/app/(app)/settings/layout.test.tsx +0 -39
- package/app/(app)/settings/layout.tsx +0 -71
- package/app/(app)/settings/page.tsx +0 -4
- package/app/(app)/settings/security/page.tsx +0 -43
- package/app/(app)/settings/team/page.tsx +0 -74
- package/app/(app)/settings/workspace/page.tsx +0 -27
- package/app/(app)/tasks/[taskId]/page.tsx +0 -79
- package/app/(auth)/forgot-password/page.tsx +0 -106
- package/app/(auth)/guest/page.tsx +0 -56
- package/app/(auth)/layout.tsx +0 -13
- package/app/(auth)/login/page.tsx +0 -14
- package/app/(auth)/register/page.tsx +0 -193
- package/app/(auth)/reset-password/page.tsx +0 -138
- package/app/api/account/claim/route.tsx +0 -135
- package/app/api/admin/backfill-encryption/route.tsx +0 -43
- package/app/api/admin/license/route.tsx +0 -42
- package/app/api/auth/2fa/route.tsx +0 -148
- package/app/api/auth/[...nextauth]/route.tsx +0 -3
- package/app/api/auth/change-password/route.tsx +0 -61
- package/app/api/auth/check-2fa/route.tsx +0 -19
- package/app/api/auth/forgot-password/route.tsx +0 -65
- package/app/api/auth/reset-password/route.tsx +0 -52
- package/app/api/auth/verify-2fa/route.tsx +0 -88
- package/app/api/backup/download/db/route.ts +0 -29
- package/app/api/backup/download/notes/route.ts +0 -25
- package/app/api/backup/settings/route.ts +0 -92
- package/app/api/billing/checkout/route.tsx +0 -81
- package/app/api/billing/migrate/route.tsx +0 -163
- package/app/api/billing/portal/route.tsx +0 -24
- package/app/api/billing/setup-intent/route.tsx +0 -55
- package/app/api/billing/status/route.tsx +0 -36
- package/app/api/billing/subscribe/route.tsx +0 -85
- package/app/api/billing/webhook/route.tsx +0 -199
- package/app/api/calendar-feeds/[feedId]/route.tsx +0 -67
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +0 -37
- package/app/api/calendar-feeds/events/route.tsx +0 -82
- package/app/api/calendar-feeds/route.tsx +0 -52
- package/app/api/calendar-feeds/sync-all/route.tsx +0 -34
- package/app/api/cron/calendar-feeds/route.tsx +0 -31
- package/app/api/cron/stale-tasks/route.tsx +0 -51
- package/app/api/cron/sync/route.tsx +0 -34
- package/app/api/devices/[deviceId]/route.tsx +0 -25
- package/app/api/devices/route.tsx +0 -41
- package/app/api/export/route.tsx +0 -40
- package/app/api/feedback/route.tsx +0 -54
- package/app/api/folders/[folderId]/route.tsx +0 -51
- package/app/api/folders/route.tsx +0 -37
- package/app/api/graph/route.tsx +0 -242
- package/app/api/guest/route.tsx +0 -58
- package/app/api/health/route.tsx +0 -10
- package/app/api/holidays/countries/route.tsx +0 -14
- package/app/api/holidays/route.tsx +0 -49
- package/app/api/holidays/states/route.tsx +0 -21
- package/app/api/invites/[token]/route.tsx +0 -131
- package/app/api/invites/route.tsx +0 -74
- package/app/api/mcp/generate-token/route.tsx +0 -55
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +0 -30
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +0 -22
- package/app/api/notes/[noteId]/export/route.tsx +0 -45
- package/app/api/notes/[noteId]/route.tsx +0 -360
- package/app/api/notes/route.tsx +0 -112
- package/app/api/notifications/route.tsx +0 -44
- package/app/api/register/route.tsx +0 -67
- package/app/api/restore/route.tsx +0 -148
- package/app/api/sync/conflicts/[conflictId]/route.tsx +0 -134
- package/app/api/sync/conflicts/route.tsx +0 -48
- package/app/api/sync/status/route.tsx +0 -49
- package/app/api/sync/trigger/route.tsx +0 -15
- package/app/api/tasks/[taskId]/detail/route.tsx +0 -68
- package/app/api/tasks/[taskId]/route.tsx +0 -259
- package/app/api/tasks/bulk/route.tsx +0 -133
- package/app/api/tasks/route.tsx +0 -36
- package/app/api/workspace/active/route.tsx +0 -39
- package/app/api/workspace/create-team/route.tsx +0 -42
- package/app/api/workspace/kanban-statuses/route.tsx +0 -71
- package/app/api/workspace/members/[memberId]/route.tsx +0 -69
- package/app/api/workspace/route.tsx +0 -24
- package/app/download/page.tsx +0 -170
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +0 -1
- package/app/generated/prisma/client.js +0 -5
- package/app/generated/prisma/default.d.ts +0 -1
- package/app/generated/prisma/default.js +0 -5
- package/app/generated/prisma/edge.d.ts +0 -1
- package/app/generated/prisma/edge.js +0 -497
- package/app/generated/prisma/index-browser.js +0 -523
- package/app/generated/prisma/index.d.ts +0 -46376
- package/app/generated/prisma/index.js +0 -497
- package/app/generated/prisma/package.json +0 -144
- package/app/generated/prisma/query_compiler_fast_bg.js +0 -2
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +0 -2
- package/app/generated/prisma/runtime/client.d.ts +0 -3386
- package/app/generated/prisma/runtime/client.js +0 -86
- package/app/generated/prisma/runtime/index-browser.d.ts +0 -90
- package/app/generated/prisma/runtime/index-browser.js +0 -6
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +0 -76
- package/app/generated/prisma/schema.prisma +0 -456
- package/app/generated/prisma/wasm-edge-light-loader.mjs +0 -5
- package/app/generated/prisma/wasm-worker-loader.mjs +0 -5
- package/app/globals.css +0 -54
- package/app/invite/[token]/page.tsx +0 -52
- package/app/layout.tsx +0 -90
- package/app/mcp/route.tsx +0 -430
- package/app/opengraph-image.tsx +0 -120
- package/app/page.tsx +0 -398
- package/app/privacy/page.tsx +0 -69
- package/app/robots.tsx +0 -25
- package/app/sitemap.tsx +0 -36
- package/app/terms/page.tsx +0 -69
- package/app/upgrade/page.tsx +0 -75
- package/auth.config.ts +0 -33
- package/auth.ts +0 -79
- package/components/auth/login-form.tsx +0 -302
- package/components/auth/password-checklist.tsx +0 -31
- package/components/auth/password-input.tsx +0 -36
- package/components/auth/switch-account-button.test.tsx +0 -22
- package/components/auth/switch-account-button.tsx +0 -19
- package/components/auth/two-factor-input.tsx +0 -116
- package/components/billing/billing-dashboard.tsx +0 -265
- package/components/billing/card-form.tsx +0 -210
- package/components/billing/claim-account-form.tsx +0 -99
- package/components/branding/app-logo.test.tsx +0 -20
- package/components/branding/app-logo.tsx +0 -25
- package/components/calendar/calendar-agenda.tsx +0 -150
- package/components/calendar/calendar-drag.test.tsx +0 -177
- package/components/calendar/calendar-grid.tsx +0 -357
- package/components/calendar/calendar-hooks.test.tsx +0 -27
- package/components/calendar/calendar-hooks.ts +0 -351
- package/components/calendar/calendar-toolbar.test.tsx +0 -68
- package/components/calendar/calendar-toolbar.tsx +0 -291
- package/components/calendar/calendar-types.ts +0 -148
- package/components/calendar/calendar-view.test.tsx +0 -295
- package/components/calendar/calendar-view.tsx +0 -307
- package/components/calendar/day-detail-popover.tsx +0 -174
- package/components/calendar/task-chip.tsx +0 -86
- package/components/command/command-palette.test.tsx +0 -33
- package/components/command/command-palette.tsx +0 -310
- package/components/download-cta.tsx +0 -87
- package/components/feedback/feedback-popup.tsx +0 -207
- package/components/graph/graph-draw.ts +0 -337
- package/components/graph/graph-overlays.tsx +0 -160
- package/components/graph/graph-page.test.tsx +0 -131
- package/components/graph/graph-page.tsx +0 -263
- package/components/graph/graph-types.ts +0 -47
- package/components/graph/graph-view.tsx +0 -322
- package/components/guide/guide-view.tsx +0 -522
- package/components/kanban/kanban-board.test.tsx +0 -128
- package/components/kanban/kanban-board.tsx +0 -361
- package/components/kanban/kanban-card-menu.tsx +0 -102
- package/components/kanban/kanban-card.tsx +0 -227
- package/components/kanban/kanban-column.tsx +0 -49
- package/components/kanban/kanban-status-context.tsx +0 -28
- package/components/landing/calendar-sandbox.test.tsx +0 -15
- package/components/landing/calendar-sandbox.tsx +0 -107
- package/components/landing/graph-sandbox.test.tsx +0 -27
- package/components/landing/graph-sandbox.tsx +0 -80
- package/components/landing/kanban-sandbox.test.tsx +0 -24
- package/components/landing/kanban-sandbox.tsx +0 -101
- package/components/landing/landing-showcase.test.tsx +0 -21
- package/components/landing/landing-showcase.tsx +0 -54
- package/components/landing/list-sandbox.tsx +0 -86
- package/components/landing/mock-workspace.ts +0 -168
- package/components/landing/notes-sandbox.test.tsx +0 -14
- package/components/landing/notes-sandbox.tsx +0 -88
- package/components/layout/app-shell.tsx +0 -83
- package/components/layout/backup-scheduler.tsx +0 -122
- package/components/layout/bottom-nav.tsx +0 -43
- package/components/layout/icon-bar.test.tsx +0 -29
- package/components/layout/icon-bar.tsx +0 -118
- package/components/layout/mobile-top-bar.tsx +0 -68
- package/components/layout/notes-panel-folder.tsx +0 -127
- package/components/layout/notes-panel-note-item.tsx +0 -140
- package/components/layout/notes-panel-task-tab.tsx +0 -63
- package/components/layout/notes-panel-types.ts +0 -44
- package/components/layout/notes-panel.tsx +0 -476
- package/components/layout/notification-bell.tsx +0 -251
- package/components/layout/paywall-screen.tsx +0 -41
- package/components/layout/pro-banner.tsx +0 -76
- package/components/layout/sw-register.tsx +0 -27
- package/components/layout/workspace-switcher.tsx +0 -90
- package/components/notes/mobile-bottom-sheet.tsx +0 -99
- package/components/notes/note-editor-context-menu.tsx +0 -47
- package/components/notes/note-editor-dom.ts +0 -33
- package/components/notes/note-editor-dropdowns.tsx +0 -484
- package/components/notes/note-editor-hooks.ts +0 -692
- package/components/notes/note-editor-keyboard.ts +0 -305
- package/components/notes/note-editor-overlay.tsx +0 -90
- package/components/notes/note-editor.test.tsx +0 -372
- package/components/notes/note-editor.tsx +0 -662
- package/components/notes/note-preview-pane.tsx +0 -156
- package/components/notes/note-tabs.tsx +0 -120
- package/components/notes/note-types.tsx +0 -157
- package/components/settings/accept-invite.tsx +0 -108
- package/components/settings/agent-token-settings.tsx +0 -369
- package/components/settings/backup-restore-settings.test.tsx +0 -25
- package/components/settings/backup-restore-settings.tsx +0 -327
- package/components/settings/calendar-feeds-settings.tsx +0 -489
- package/components/settings/calendar-general-settings.tsx +0 -174
- package/components/settings/confirm-danger-action.test.tsx +0 -215
- package/components/settings/confirm-danger-action.tsx +0 -65
- package/components/settings/security-settings.tsx +0 -252
- package/components/settings/settings-guidance.test.tsx +0 -98
- package/components/settings/team-settings.tsx +0 -319
- package/components/settings/two-factor-auth.tsx +0 -296
- package/components/settings/workspace-settings-client.tsx +0 -363
- package/components/settings/workspace-settings-form.tsx +0 -73
- package/components/sync/conflict-viewer.tsx +0 -247
- package/components/sync/sync-indicator.tsx +0 -171
- package/components/tasks/snippet-thread.tsx +0 -119
- package/components/tasks/status-dot.tsx +0 -47
- package/components/tasks/task-badge.tsx +0 -43
- package/components/tasks/task-detail.test.tsx +0 -187
- package/components/tasks/task-detail.tsx +0 -458
- package/components/tasks/task-list-filters.test.tsx +0 -75
- package/components/tasks/task-list-filters.tsx +0 -163
- package/components/tasks/task-list-types.ts +0 -20
- package/components/tasks/task-list.test.tsx +0 -175
- package/components/tasks/task-list.tsx +0 -481
- package/components/tasks/task-row.tsx +0 -85
- package/components/tasks/task-table-row.tsx +0 -259
- package/components/ui/skeleton.tsx +0 -3
- package/components/ui/toast.test.tsx +0 -42
- package/components/ui/toast.tsx +0 -70
- package/electron/main.ts +0 -251
- package/electron/preload.ts +0 -56
- package/instrumentation.tsx +0 -23
- package/lib/api-error.ts +0 -50
- package/lib/backup/backup-runner.test.ts +0 -32
- package/lib/backup/backup-runner.ts +0 -19
- package/lib/backup/backup-schedule.test.ts +0 -23
- package/lib/backup/backup-schedule.ts +0 -55
- package/lib/backup/backup-settings.test.ts +0 -30
- package/lib/backup/backup-settings.ts +0 -27
- package/lib/backup/export-notes-zip.test.ts +0 -26
- package/lib/backup/export-notes-zip.ts +0 -82
- package/lib/backup/export-workspace-backup.test.ts +0 -17
- package/lib/backup/export-workspace-backup.ts +0 -77
- package/lib/backup/restore-workspace-from-export.test.ts +0 -18
- package/lib/backup/restore-workspace-from-export.ts +0 -183
- package/lib/backup/types.ts +0 -14
- package/lib/brand-icons.ts +0 -1
- package/lib/calendar-feed-crypto.ts +0 -38
- package/lib/calendar-feed.ts +0 -239
- package/lib/client/online-status.ts +0 -47
- package/lib/conflict-resolver.test.ts +0 -57
- package/lib/conflict-resolver.ts +0 -240
- package/lib/db-init.ts +0 -79
- package/lib/email.ts +0 -159
- package/lib/encryption.test.ts +0 -41
- package/lib/encryption.ts +0 -98
- package/lib/extract-snippet.test.ts +0 -123
- package/lib/extract-snippet.ts +0 -69
- package/lib/kanban-status.ts +0 -55
- package/lib/license.ts +0 -21
- package/lib/limits.ts +0 -31
- package/lib/mcp-auth.test.ts +0 -58
- package/lib/mcp-auth.ts +0 -65
- package/lib/mcp-contract.test.ts +0 -25
- package/lib/mcp-contract.ts +0 -210
- package/lib/mcp-handler.ts +0 -31
- package/lib/mcp-url.test.ts +0 -12
- package/lib/mcp-url.ts +0 -7
- package/lib/mentions.test.ts +0 -45
- package/lib/mentions.ts +0 -73
- package/lib/note-crypto.ts +0 -108
- package/lib/note-sync.ts +0 -201
- package/lib/note-title.ts +0 -93
- package/lib/prisma.ts +0 -193
- package/lib/pro-flush.ts +0 -292
- package/lib/rate-limit.ts +0 -57
- package/lib/stripe.ts +0 -38
- package/lib/sync-worker.ts +0 -388
- package/lib/task-parser.test.ts +0 -91
- package/lib/task-parser.ts +0 -81
- package/lib/task-utils.ts +0 -52
- package/lib/use-is-electron.ts +0 -19
- package/lib/use-is-mobile.ts +0 -22
- package/lib/validation/calendar-feed.ts +0 -31
- package/lib/validation/note.ts +0 -27
- package/lib/validation/task.ts +0 -26
- package/lib/view-preferences.test.ts +0 -54
- package/lib/view-preferences.ts +0 -28
- package/lib/workspace.ts +0 -66
- package/next.config.ts +0 -21
- package/postcss.config.mjs +0 -7
- package/prisma/migrations/20260519021916_init/migration.sql +0 -388
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +0 -8
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +0 -2
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +0 -12
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +0 -3
- package/prisma/migrations/20260529030000_add_folders/migration.sql +0 -17
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +0 -31
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +0 -5
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +0 -14
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +0 -26
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +0 -23
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +0 -43
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +0 -2
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +0 -14
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +0 -2
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +0 -25
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +0 -20
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +0 -4
- package/prisma/migrations/migration_lock.toml +0 -3
- package/prisma/schema.prisma +0 -457
- package/public/Logo_icon.svg +0 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +0 -4
- package/public/icon_dark.svg +0 -1
- package/public/knotpad_icon.svg +0 -1
- package/public/knotpad_logo_full.svg +0 -1
- package/public/manifest.json +0 -14
- package/public/next.svg +0 -1
- package/public/sw.js +0 -137
- package/public/vercel.svg +0 -1
- package/public/window.svg +0 -1
- 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
|
-
}
|
package/app/opengraph-image.tsx
DELETED
|
@@ -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
|
-
}
|