@knotpad/app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -0
- package/app/(app)/calendar/page.tsx +57 -0
- package/app/(app)/error.tsx +35 -0
- package/app/(app)/graph/page.tsx +32 -0
- package/app/(app)/guide/page.tsx +21 -0
- package/app/(app)/kanban/loading.tsx +24 -0
- package/app/(app)/kanban/page.tsx +59 -0
- package/app/(app)/layout.tsx +122 -0
- package/app/(app)/list/loading.tsx +21 -0
- package/app/(app)/list/page.tsx +137 -0
- package/app/(app)/loading.tsx +18 -0
- package/app/(app)/notes/[noteId]/page.tsx +84 -0
- package/app/(app)/notes/layout.tsx +30 -0
- package/app/(app)/notes/page.tsx +39 -0
- package/app/(app)/page.tsx +5 -0
- package/app/(app)/settings/agent-token/page.tsx +59 -0
- package/app/(app)/settings/backup/page.tsx +49 -0
- package/app/(app)/settings/billing/page.tsx +53 -0
- package/app/(app)/settings/calendar/page.tsx +41 -0
- package/app/(app)/settings/layout.test.tsx +39 -0
- package/app/(app)/settings/layout.tsx +71 -0
- package/app/(app)/settings/page.tsx +4 -0
- package/app/(app)/settings/security/page.tsx +43 -0
- package/app/(app)/settings/team/page.tsx +74 -0
- package/app/(app)/settings/workspace/page.tsx +27 -0
- package/app/(app)/tasks/[taskId]/page.tsx +79 -0
- package/app/(auth)/forgot-password/page.tsx +106 -0
- package/app/(auth)/guest/page.tsx +56 -0
- package/app/(auth)/layout.tsx +13 -0
- package/app/(auth)/login/page.tsx +14 -0
- package/app/(auth)/register/page.tsx +193 -0
- package/app/(auth)/reset-password/page.tsx +138 -0
- package/app/api/account/claim/route.tsx +135 -0
- package/app/api/admin/backfill-encryption/route.tsx +43 -0
- package/app/api/admin/license/route.tsx +42 -0
- package/app/api/auth/2fa/route.tsx +148 -0
- package/app/api/auth/[...nextauth]/route.tsx +3 -0
- package/app/api/auth/change-password/route.tsx +61 -0
- package/app/api/auth/check-2fa/route.tsx +19 -0
- package/app/api/auth/forgot-password/route.tsx +65 -0
- package/app/api/auth/reset-password/route.tsx +52 -0
- package/app/api/auth/verify-2fa/route.tsx +88 -0
- package/app/api/backup/download/db/route.ts +29 -0
- package/app/api/backup/download/notes/route.ts +25 -0
- package/app/api/backup/settings/route.ts +92 -0
- package/app/api/billing/checkout/route.tsx +81 -0
- package/app/api/billing/migrate/route.tsx +163 -0
- package/app/api/billing/portal/route.tsx +24 -0
- package/app/api/billing/setup-intent/route.tsx +55 -0
- package/app/api/billing/status/route.tsx +36 -0
- package/app/api/billing/subscribe/route.tsx +85 -0
- package/app/api/billing/webhook/route.tsx +199 -0
- package/app/api/calendar-feeds/[feedId]/route.tsx +67 -0
- package/app/api/calendar-feeds/[feedId]/sync/route.tsx +37 -0
- package/app/api/calendar-feeds/events/route.tsx +82 -0
- package/app/api/calendar-feeds/route.tsx +52 -0
- package/app/api/calendar-feeds/sync-all/route.tsx +34 -0
- package/app/api/cron/calendar-feeds/route.tsx +31 -0
- package/app/api/cron/stale-tasks/route.tsx +51 -0
- package/app/api/cron/sync/route.tsx +34 -0
- package/app/api/devices/[deviceId]/route.tsx +25 -0
- package/app/api/devices/route.tsx +41 -0
- package/app/api/export/route.tsx +40 -0
- package/app/api/feedback/route.tsx +54 -0
- package/app/api/folders/[folderId]/route.tsx +51 -0
- package/app/api/folders/route.tsx +37 -0
- package/app/api/graph/route.tsx +242 -0
- package/app/api/guest/route.tsx +58 -0
- package/app/api/health/route.tsx +10 -0
- package/app/api/holidays/countries/route.tsx +14 -0
- package/app/api/holidays/route.tsx +49 -0
- package/app/api/holidays/states/route.tsx +21 -0
- package/app/api/invites/[token]/route.tsx +131 -0
- package/app/api/invites/route.tsx +74 -0
- package/app/api/mcp/generate-token/route.tsx +55 -0
- package/app/api/mcp/revoke-token/[tokenId]/route.tsx +30 -0
- package/app/api/mcp/update-alias/[tokenId]/route.tsx +22 -0
- package/app/api/notes/[noteId]/export/route.tsx +45 -0
- package/app/api/notes/[noteId]/route.tsx +360 -0
- package/app/api/notes/route.tsx +112 -0
- package/app/api/notifications/route.tsx +44 -0
- package/app/api/register/route.tsx +67 -0
- package/app/api/restore/route.tsx +148 -0
- package/app/api/sync/conflicts/[conflictId]/route.tsx +134 -0
- package/app/api/sync/conflicts/route.tsx +48 -0
- package/app/api/sync/status/route.tsx +49 -0
- package/app/api/sync/trigger/route.tsx +15 -0
- package/app/api/tasks/[taskId]/detail/route.tsx +68 -0
- package/app/api/tasks/[taskId]/route.tsx +259 -0
- package/app/api/tasks/bulk/route.tsx +133 -0
- package/app/api/tasks/route.tsx +36 -0
- package/app/api/workspace/active/route.tsx +39 -0
- package/app/api/workspace/create-team/route.tsx +42 -0
- package/app/api/workspace/kanban-statuses/route.tsx +71 -0
- package/app/api/workspace/members/[memberId]/route.tsx +69 -0
- package/app/api/workspace/route.tsx +24 -0
- package/app/download/page.tsx +170 -0
- package/app/favicon.ico +0 -0
- package/app/generated/prisma/client.d.ts +1 -0
- package/app/generated/prisma/client.js +5 -0
- package/app/generated/prisma/default.d.ts +1 -0
- package/app/generated/prisma/default.js +5 -0
- package/app/generated/prisma/edge.d.ts +1 -0
- package/app/generated/prisma/edge.js +497 -0
- package/app/generated/prisma/index-browser.js +523 -0
- package/app/generated/prisma/index.d.ts +46376 -0
- package/app/generated/prisma/index.js +497 -0
- package/app/generated/prisma/package.json +144 -0
- package/app/generated/prisma/query_compiler_fast_bg.js +2 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm +0 -0
- package/app/generated/prisma/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/app/generated/prisma/runtime/client.d.ts +3386 -0
- package/app/generated/prisma/runtime/client.js +86 -0
- package/app/generated/prisma/runtime/index-browser.d.ts +90 -0
- package/app/generated/prisma/runtime/index-browser.js +6 -0
- package/app/generated/prisma/runtime/wasm-compiler-edge.js +76 -0
- package/app/generated/prisma/schema.prisma +456 -0
- package/app/generated/prisma/wasm-edge-light-loader.mjs +5 -0
- package/app/generated/prisma/wasm-worker-loader.mjs +5 -0
- package/app/globals.css +54 -0
- package/app/invite/[token]/page.tsx +52 -0
- package/app/layout.tsx +90 -0
- package/app/mcp/route.tsx +430 -0
- package/app/opengraph-image.tsx +120 -0
- package/app/page.tsx +398 -0
- package/app/privacy/page.tsx +69 -0
- package/app/robots.tsx +25 -0
- package/app/sitemap.tsx +36 -0
- package/app/terms/page.tsx +69 -0
- package/app/upgrade/page.tsx +75 -0
- package/auth.config.ts +33 -0
- package/auth.ts +79 -0
- package/bin/brief.js +224 -0
- package/components/auth/login-form.tsx +302 -0
- package/components/auth/password-checklist.tsx +31 -0
- package/components/auth/password-input.tsx +36 -0
- package/components/auth/switch-account-button.test.tsx +22 -0
- package/components/auth/switch-account-button.tsx +19 -0
- package/components/auth/two-factor-input.tsx +116 -0
- package/components/billing/billing-dashboard.tsx +265 -0
- package/components/billing/card-form.tsx +210 -0
- package/components/billing/claim-account-form.tsx +99 -0
- package/components/branding/app-logo.test.tsx +20 -0
- package/components/branding/app-logo.tsx +25 -0
- package/components/calendar/calendar-agenda.tsx +150 -0
- package/components/calendar/calendar-drag.test.tsx +177 -0
- package/components/calendar/calendar-grid.tsx +357 -0
- package/components/calendar/calendar-hooks.test.tsx +27 -0
- package/components/calendar/calendar-hooks.ts +351 -0
- package/components/calendar/calendar-toolbar.test.tsx +68 -0
- package/components/calendar/calendar-toolbar.tsx +291 -0
- package/components/calendar/calendar-types.ts +148 -0
- package/components/calendar/calendar-view.test.tsx +295 -0
- package/components/calendar/calendar-view.tsx +307 -0
- package/components/calendar/day-detail-popover.tsx +174 -0
- package/components/calendar/task-chip.tsx +86 -0
- package/components/command/command-palette.test.tsx +33 -0
- package/components/command/command-palette.tsx +310 -0
- package/components/download-cta.tsx +87 -0
- package/components/feedback/feedback-popup.tsx +207 -0
- package/components/graph/graph-draw.ts +337 -0
- package/components/graph/graph-overlays.tsx +160 -0
- package/components/graph/graph-page.test.tsx +131 -0
- package/components/graph/graph-page.tsx +263 -0
- package/components/graph/graph-types.ts +47 -0
- package/components/graph/graph-view.tsx +322 -0
- package/components/guide/guide-view.tsx +522 -0
- package/components/kanban/kanban-board.test.tsx +128 -0
- package/components/kanban/kanban-board.tsx +361 -0
- package/components/kanban/kanban-card-menu.tsx +102 -0
- package/components/kanban/kanban-card.tsx +227 -0
- package/components/kanban/kanban-column.tsx +49 -0
- package/components/kanban/kanban-status-context.tsx +28 -0
- package/components/landing/calendar-sandbox.test.tsx +15 -0
- package/components/landing/calendar-sandbox.tsx +107 -0
- package/components/landing/graph-sandbox.test.tsx +27 -0
- package/components/landing/graph-sandbox.tsx +80 -0
- package/components/landing/kanban-sandbox.test.tsx +24 -0
- package/components/landing/kanban-sandbox.tsx +101 -0
- package/components/landing/landing-showcase.test.tsx +21 -0
- package/components/landing/landing-showcase.tsx +54 -0
- package/components/landing/list-sandbox.tsx +86 -0
- package/components/landing/mock-workspace.ts +168 -0
- package/components/landing/notes-sandbox.test.tsx +14 -0
- package/components/landing/notes-sandbox.tsx +88 -0
- package/components/layout/app-shell.tsx +83 -0
- package/components/layout/backup-scheduler.tsx +122 -0
- package/components/layout/bottom-nav.tsx +43 -0
- package/components/layout/icon-bar.test.tsx +29 -0
- package/components/layout/icon-bar.tsx +118 -0
- package/components/layout/mobile-top-bar.tsx +68 -0
- package/components/layout/notes-panel-folder.tsx +127 -0
- package/components/layout/notes-panel-note-item.tsx +140 -0
- package/components/layout/notes-panel-task-tab.tsx +63 -0
- package/components/layout/notes-panel-types.ts +44 -0
- package/components/layout/notes-panel.tsx +476 -0
- package/components/layout/notification-bell.tsx +251 -0
- package/components/layout/paywall-screen.tsx +41 -0
- package/components/layout/pro-banner.tsx +76 -0
- package/components/layout/sw-register.tsx +27 -0
- package/components/layout/workspace-switcher.tsx +90 -0
- package/components/notes/mobile-bottom-sheet.tsx +99 -0
- package/components/notes/note-editor-context-menu.tsx +47 -0
- package/components/notes/note-editor-dom.ts +33 -0
- package/components/notes/note-editor-dropdowns.tsx +484 -0
- package/components/notes/note-editor-hooks.ts +692 -0
- package/components/notes/note-editor-keyboard.ts +305 -0
- package/components/notes/note-editor-overlay.tsx +90 -0
- package/components/notes/note-editor.test.tsx +372 -0
- package/components/notes/note-editor.tsx +662 -0
- package/components/notes/note-preview-pane.tsx +156 -0
- package/components/notes/note-tabs.tsx +120 -0
- package/components/notes/note-types.tsx +157 -0
- package/components/settings/accept-invite.tsx +108 -0
- package/components/settings/agent-token-settings.tsx +369 -0
- package/components/settings/backup-restore-settings.test.tsx +25 -0
- package/components/settings/backup-restore-settings.tsx +327 -0
- package/components/settings/calendar-feeds-settings.tsx +489 -0
- package/components/settings/calendar-general-settings.tsx +174 -0
- package/components/settings/confirm-danger-action.test.tsx +215 -0
- package/components/settings/confirm-danger-action.tsx +65 -0
- package/components/settings/security-settings.tsx +252 -0
- package/components/settings/settings-guidance.test.tsx +98 -0
- package/components/settings/team-settings.tsx +319 -0
- package/components/settings/two-factor-auth.tsx +296 -0
- package/components/settings/workspace-settings-client.tsx +363 -0
- package/components/settings/workspace-settings-form.tsx +73 -0
- package/components/sync/conflict-viewer.tsx +247 -0
- package/components/sync/sync-indicator.tsx +171 -0
- package/components/tasks/snippet-thread.tsx +119 -0
- package/components/tasks/status-dot.tsx +47 -0
- package/components/tasks/task-badge.tsx +43 -0
- package/components/tasks/task-detail.test.tsx +187 -0
- package/components/tasks/task-detail.tsx +458 -0
- package/components/tasks/task-list-filters.test.tsx +75 -0
- package/components/tasks/task-list-filters.tsx +163 -0
- package/components/tasks/task-list-types.ts +20 -0
- package/components/tasks/task-list.test.tsx +175 -0
- package/components/tasks/task-list.tsx +481 -0
- package/components/tasks/task-row.tsx +85 -0
- package/components/tasks/task-table-row.tsx +259 -0
- package/components/ui/skeleton.tsx +3 -0
- package/components/ui/toast.test.tsx +42 -0
- package/components/ui/toast.tsx +70 -0
- package/electron/main.ts +251 -0
- package/electron/preload.ts +56 -0
- package/instrumentation.tsx +23 -0
- package/lib/api-error.ts +50 -0
- package/lib/backup/backup-runner.test.ts +32 -0
- package/lib/backup/backup-runner.ts +19 -0
- package/lib/backup/backup-schedule.test.ts +23 -0
- package/lib/backup/backup-schedule.ts +55 -0
- package/lib/backup/backup-settings.test.ts +30 -0
- package/lib/backup/backup-settings.ts +27 -0
- package/lib/backup/export-notes-zip.test.ts +26 -0
- package/lib/backup/export-notes-zip.ts +82 -0
- package/lib/backup/export-workspace-backup.test.ts +17 -0
- package/lib/backup/export-workspace-backup.ts +77 -0
- package/lib/backup/restore-workspace-from-export.test.ts +18 -0
- package/lib/backup/restore-workspace-from-export.ts +183 -0
- package/lib/backup/types.ts +14 -0
- package/lib/brand-icons.ts +1 -0
- package/lib/calendar-feed-crypto.ts +38 -0
- package/lib/calendar-feed.ts +239 -0
- package/lib/client/online-status.ts +47 -0
- package/lib/conflict-resolver.test.ts +57 -0
- package/lib/conflict-resolver.ts +240 -0
- package/lib/db-init.ts +79 -0
- package/lib/email.ts +159 -0
- package/lib/encryption.test.ts +41 -0
- package/lib/encryption.ts +98 -0
- package/lib/extract-snippet.test.ts +123 -0
- package/lib/extract-snippet.ts +69 -0
- package/lib/kanban-status.ts +55 -0
- package/lib/license.ts +21 -0
- package/lib/limits.ts +31 -0
- package/lib/mcp-auth.test.ts +58 -0
- package/lib/mcp-auth.ts +65 -0
- package/lib/mcp-contract.test.ts +25 -0
- package/lib/mcp-contract.ts +210 -0
- package/lib/mcp-handler.ts +31 -0
- package/lib/mcp-url.test.ts +12 -0
- package/lib/mcp-url.ts +7 -0
- package/lib/mentions.test.ts +45 -0
- package/lib/mentions.ts +73 -0
- package/lib/note-crypto.ts +108 -0
- package/lib/note-sync.ts +201 -0
- package/lib/note-title.ts +93 -0
- package/lib/prisma.ts +193 -0
- package/lib/pro-flush.ts +292 -0
- package/lib/rate-limit.ts +57 -0
- package/lib/stripe.ts +38 -0
- package/lib/sync-worker.ts +388 -0
- package/lib/task-parser.test.ts +91 -0
- package/lib/task-parser.ts +81 -0
- package/lib/task-utils.ts +52 -0
- package/lib/use-is-electron.ts +19 -0
- package/lib/use-is-mobile.ts +22 -0
- package/lib/validation/calendar-feed.ts +31 -0
- package/lib/validation/note.ts +27 -0
- package/lib/validation/task.ts +26 -0
- package/lib/view-preferences.test.ts +54 -0
- package/lib/view-preferences.ts +28 -0
- package/lib/workspace.ts +66 -0
- package/next.config.ts +21 -0
- package/package.json +99 -0
- package/postcss.config.mjs +7 -0
- package/prisma/migrations/20260519021916_init/migration.sql +388 -0
- package/prisma/migrations/20260519061113_drop_sync_password/migration.sql +8 -0
- package/prisma/migrations/20260520065016_add_task_start_date/migration.sql +2 -0
- package/prisma/migrations/20260529010600_remove_encryption_fields/migration.sql +12 -0
- package/prisma/migrations/20260529020000_restore_encryption_salt/migration.sql +3 -0
- package/prisma/migrations/20260529030000_add_folders/migration.sql +17 -0
- package/prisma/migrations/20260605000000_deferred_fixes/migration.sql +31 -0
- package/prisma/migrations/20260605020806_add_pending_sync_to_note_and_task/migration.sql +5 -0
- package/prisma/migrations/20260605063634_add_stripe_webhook_event_sync_lock/migration.sql +14 -0
- package/prisma/migrations/20260605100000_add_prod_indexes/migration.sql +26 -0
- package/prisma/migrations/20260608081404_add_kanban_statuses/migration.sql +23 -0
- package/prisma/migrations/20260611032723_add_calendar_feeds/migration.sql +43 -0
- package/prisma/migrations/20260611040000_add_calendar_feed_color/migration.sql +2 -0
- package/prisma/migrations/20260611050000_add_task_priority/migration.sql +14 -0
- package/prisma/migrations/20260612060000_add_critical_priority/migration.sql +2 -0
- package/prisma/migrations/20260613090000_add_backup_settings/migration.sql +25 -0
- package/prisma/migrations/20260614160000_add_feedback/migration.sql +20 -0
- package/prisma/migrations/20260614210000_add_2fa/migration.sql +4 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +457 -0
- package/public/Logo_icon.svg +1 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +4 -0
- package/public/icon_dark.svg +1 -0
- package/public/knotpad_icon.svg +1 -0
- package/public/knotpad_logo_full.svg +1 -0
- package/public/manifest.json +14 -0
- package/public/next.svg +1 -0
- package/public/sw.js +137 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +35 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { extractSnippet } from "./extract-snippet";
|
|
3
|
+
|
|
4
|
+
describe("extractSnippet", () => {
|
|
5
|
+
it("extracts snippet from a checkbox task line", () => {
|
|
6
|
+
const content = [
|
|
7
|
+
"# Meeting notes",
|
|
8
|
+
"- [ ] ship docs @alice",
|
|
9
|
+
"- [ ] review PR",
|
|
10
|
+
"Some follow-up text",
|
|
11
|
+
].join("\n");
|
|
12
|
+
|
|
13
|
+
const result = extractSnippet(content, "ship docs");
|
|
14
|
+
expect(result).toContain("- [ ] ship docs @alice");
|
|
15
|
+
expect(result).toContain("# Meeting notes");
|
|
16
|
+
expect(result).toContain("- [ ] review PR");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("extracts snippet from a ((task title)) wikilink in plain text", () => {
|
|
20
|
+
const content = [
|
|
21
|
+
"Plan for the week:",
|
|
22
|
+
"We need to ((ship docs)) before Friday.",
|
|
23
|
+
"Then we can relax.",
|
|
24
|
+
].join("\n");
|
|
25
|
+
|
|
26
|
+
const result = extractSnippet(content, "ship docs");
|
|
27
|
+
expect(result).toContain("We need to ship docs before Friday.");
|
|
28
|
+
expect(result).not.toContain("((ship docs))");
|
|
29
|
+
expect(result).toContain("Plan for the week:");
|
|
30
|
+
expect(result).toContain("Then we can relax.");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("extracts snippet from a ((task title)) wikilink in a bullet list", () => {
|
|
34
|
+
const content = [
|
|
35
|
+
"- First item",
|
|
36
|
+
"- Check ((ship docs)) status",
|
|
37
|
+
"- Last item",
|
|
38
|
+
].join("\n");
|
|
39
|
+
|
|
40
|
+
const result = extractSnippet(content, "ship docs");
|
|
41
|
+
expect(result).toContain("- Check ship docs status");
|
|
42
|
+
expect(result).not.toContain("((ship docs))");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("finds multiple references in one note", () => {
|
|
46
|
+
const content = [
|
|
47
|
+
"Line one",
|
|
48
|
+
"((ship docs))",
|
|
49
|
+
"Line three",
|
|
50
|
+
"Line four",
|
|
51
|
+
"Line five",
|
|
52
|
+
"((ship docs)) again",
|
|
53
|
+
"Line seven",
|
|
54
|
+
].join("\n");
|
|
55
|
+
|
|
56
|
+
const result = extractSnippet(content, "ship docs");
|
|
57
|
+
const blocks = result.split("\n---\n");
|
|
58
|
+
expect(blocks).toHaveLength(2);
|
|
59
|
+
expect(blocks[0]).toContain("ship docs");
|
|
60
|
+
expect(blocks[1]).toContain("ship docs");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns empty string when there is no match", () => {
|
|
64
|
+
const content = "Just some random text.\nNothing here.";
|
|
65
|
+
expect(extractSnippet(content, "nonexistent task")).toBe("");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("does not duplicate when a checkbox line also contains a wikilink", () => {
|
|
69
|
+
const content = [
|
|
70
|
+
"- [ ] ((ship docs)) @alice",
|
|
71
|
+
"Next line",
|
|
72
|
+
].join("\n");
|
|
73
|
+
|
|
74
|
+
const result = extractSnippet(content, "ship docs");
|
|
75
|
+
const blocks = result.split("\n---\n").filter(Boolean);
|
|
76
|
+
expect(blocks).toHaveLength(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("strips wikilink syntax even when it spans multiple lines in a block", () => {
|
|
80
|
+
const content = [
|
|
81
|
+
"Context line",
|
|
82
|
+
"See ((ship docs)) for details",
|
|
83
|
+
"More context",
|
|
84
|
+
].join("\n");
|
|
85
|
+
|
|
86
|
+
const result = extractSnippet(content, "ship docs");
|
|
87
|
+
expect(result).not.toContain("((ship docs))");
|
|
88
|
+
expect(result).toContain("See ship docs for details");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("finds checkbox lines that contain [[note refs]] and dates", () => {
|
|
92
|
+
const content = [
|
|
93
|
+
"# Meeting notes",
|
|
94
|
+
"- [ ] review [[Spec]] ((Task)) @bob <file.md> 2026-06-01..2026-06-05",
|
|
95
|
+
"- [ ] something else",
|
|
96
|
+
].join("\n");
|
|
97
|
+
|
|
98
|
+
const result = extractSnippet(content, "review");
|
|
99
|
+
expect(result).toContain(
|
|
100
|
+
"- [ ] review [[Spec]] ((Task)) @bob <file.md> 2026-06-01..2026-06-05"
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("finds checkbox lines that contain a single date", () => {
|
|
105
|
+
const content = [
|
|
106
|
+
"- [ ] ship docs 2026-05-20",
|
|
107
|
+
"- [ ] other task",
|
|
108
|
+
].join("\n");
|
|
109
|
+
|
|
110
|
+
const result = extractSnippet(content, "ship docs");
|
|
111
|
+
expect(result).toContain("- [ ] ship docs 2026-05-20");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("does not collapse whitespace when matching checkbox titles", () => {
|
|
115
|
+
const content = [
|
|
116
|
+
"- [ ] ship docs",
|
|
117
|
+
"- [ ] other task",
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
const result = extractSnippet(content, "ship docs");
|
|
121
|
+
expect(result).toContain("- [ ] ship docs");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function escapeRegExp(str: string): string {
|
|
2
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const MENTION_RE = /@[\w-]+/g;
|
|
6
|
+
const FILEREF_RE = /<[^>]+>/g;
|
|
7
|
+
const NOTE_REF_RE = /\[\[[^\]]*\]\]/g;
|
|
8
|
+
const TASK_REF_RE = /\(\([^)]*\)\)/g;
|
|
9
|
+
const STATUS_COMMENT_RE = /\s*<!--task::[A-Z_]+-->/g;
|
|
10
|
+
const DATE_RANGE_RE = /\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}/g;
|
|
11
|
+
const DATE_RE = /\d{4}-\d{2}-\d{2}/g;
|
|
12
|
+
|
|
13
|
+
function cleanTaskTitle(text: string): string {
|
|
14
|
+
return text
|
|
15
|
+
.replace(NOTE_REF_RE, "")
|
|
16
|
+
.replace(TASK_REF_RE, "")
|
|
17
|
+
.replace(MENTION_RE, "")
|
|
18
|
+
.replace(FILEREF_RE, "")
|
|
19
|
+
.replace(DATE_RANGE_RE, "")
|
|
20
|
+
.replace(DATE_RE, "")
|
|
21
|
+
.replace(STATUS_COMMENT_RE, "")
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function extractSnippet(content: string, taskTitle: string): string {
|
|
26
|
+
const lines = content.split("\n");
|
|
27
|
+
const usedIndices = new Set<number>();
|
|
28
|
+
const snippets: string[] = [];
|
|
29
|
+
const wikilinkPattern = `\\(\\(\\s*${escapeRegExp(taskTitle)}\\s*\\)\\)`;
|
|
30
|
+
const wikilinkRe = new RegExp(wikilinkPattern);
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
if (usedIndices.has(i)) continue;
|
|
34
|
+
|
|
35
|
+
let isMatch = false;
|
|
36
|
+
let isWikilink = false;
|
|
37
|
+
|
|
38
|
+
// Pattern 1: checkbox task line
|
|
39
|
+
const checkboxPrefix = lines[i].match(/^(\s*)-\s+\[([ x])\]\s*/);
|
|
40
|
+
if (checkboxPrefix) {
|
|
41
|
+
const textAfterCheckbox = lines[i].slice(checkboxPrefix[0].length);
|
|
42
|
+
if (cleanTaskTitle(textAfterCheckbox) === taskTitle) {
|
|
43
|
+
isMatch = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Pattern 2: wikilink reference ((task title))
|
|
48
|
+
if (!isMatch && lines[i].match(wikilinkRe)) {
|
|
49
|
+
isMatch = true;
|
|
50
|
+
isWikilink = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!isMatch) continue;
|
|
54
|
+
usedIndices.add(i);
|
|
55
|
+
|
|
56
|
+
const start = Math.max(0, i - 2);
|
|
57
|
+
const end = Math.min(lines.length, i + 3);
|
|
58
|
+
let block = lines.slice(start, end).join("\n");
|
|
59
|
+
|
|
60
|
+
if (isWikilink) {
|
|
61
|
+
// Strip ((...)) syntax so the snippet reads as natural text
|
|
62
|
+
block = block.replace(new RegExp(wikilinkPattern, "g"), taskTitle);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
snippets.push(block);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return snippets.join("\n---\n");
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { prisma } from "@/lib/prisma";
|
|
2
|
+
|
|
3
|
+
export type KanbanStatusConfig = {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
color: string;
|
|
7
|
+
order: number;
|
|
8
|
+
isVisible: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DEFAULT_STATUSES: KanbanStatusConfig[] = [
|
|
12
|
+
{ key: "OPEN", label: "Open", color: "border-zinc-700", order: 0, isVisible: true },
|
|
13
|
+
{ key: "CLAIMED", label: "Claimed", color: "border-violet-700", order: 1, isVisible: true },
|
|
14
|
+
{ key: "IN_PROGRESS", label: "In Progress", color: "border-blue-700", order: 2, isVisible: true },
|
|
15
|
+
{ key: "REVIEW", label: "Review", color: "border-amber-700", order: 3, isVisible: true },
|
|
16
|
+
{ key: "DONE", label: "Done", color: "border-emerald-700", order: 4, isVisible: true },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export async function getKanbanStatuses(workspaceId: string): Promise<KanbanStatusConfig[]> {
|
|
20
|
+
const rows = await prisma.kanbanStatus.findMany({
|
|
21
|
+
where: { workspaceId },
|
|
22
|
+
orderBy: { order: "asc" },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (rows.length === 0) {
|
|
26
|
+
return DEFAULT_STATUSES;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return rows.map((r) => ({
|
|
30
|
+
key: r.key,
|
|
31
|
+
label: r.label,
|
|
32
|
+
color: r.color,
|
|
33
|
+
order: r.order,
|
|
34
|
+
isVisible: r.isVisible,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function seedDefaultKanbanStatuses(
|
|
39
|
+
workspaceId: string,
|
|
40
|
+
tx?: Parameters<Parameters<typeof prisma.$transaction>[0]>[0]
|
|
41
|
+
) {
|
|
42
|
+
const client = tx ?? prisma;
|
|
43
|
+
const existing = await client.kanbanStatus.findFirst({
|
|
44
|
+
where: { workspaceId },
|
|
45
|
+
});
|
|
46
|
+
if (existing) return;
|
|
47
|
+
|
|
48
|
+
await client.kanbanStatus.createMany({
|
|
49
|
+
data: DEFAULT_STATUSES.map((s) => ({ ...s, workspaceId })),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getDefaultKanbanStatuses(): KanbanStatusConfig[] {
|
|
54
|
+
return DEFAULT_STATUSES.map((s) => ({ ...s }));
|
|
55
|
+
}
|
package/lib/license.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Derive Pro / Cloud entitlement from the canonical license fields rather than
|
|
2
|
+
// the denormalized `isPro` / `isCloud` boolean columns. Those booleans are a
|
|
3
|
+
// cache kept in sync by the admin license route and the Stripe webhook, but they
|
|
4
|
+
// drift when `planType` / `licenseType` are edited directly in the DB. The gating
|
|
5
|
+
// paths should treat this derivation as authoritative.
|
|
6
|
+
//
|
|
7
|
+
// Entitlement rule (mirrors prisma/schema.prisma:73-74):
|
|
8
|
+
// isPro / isCloud === planType != FREE || licenseType == COMPLIMENTARY
|
|
9
|
+
|
|
10
|
+
type LicenseFields = {
|
|
11
|
+
planType: string;
|
|
12
|
+
licenseType: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function isWorkspacePro(ws: LicenseFields): boolean {
|
|
16
|
+
return ws.planType !== "FREE" || ws.licenseType === "COMPLIMENTARY";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Today Cloud access has the same derivation as Pro. Kept as a separate export so
|
|
20
|
+
// call sites read clearly and the two can diverge later without a refactor.
|
|
21
|
+
export const isWorkspaceCloud = isWorkspacePro;
|
package/lib/limits.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Shared guard rails for note size. Kept in lib (not a route) so both route
|
|
2
|
+
// handlers and zod validation schemas can import them without a circular dep.
|
|
3
|
+
export const MAX_CONTENT_LEN = 1_000_000; // ~1 MB of markdown
|
|
4
|
+
export const MAX_TITLE_LEN = 500;
|
|
5
|
+
|
|
6
|
+
// Calendar feed (read-only .ics import) limits.
|
|
7
|
+
export const MAX_CALENDAR_FEEDS = 5;
|
|
8
|
+
export const MAX_FEED_LABEL_LEN = 60;
|
|
9
|
+
export const ICS_FETCH_TIMEOUT_MS = 10_000;
|
|
10
|
+
export const ICS_MAX_BYTES = 2_000_000; // 2 MB
|
|
11
|
+
export const CALENDAR_FEED_PAST_DAYS = 30;
|
|
12
|
+
export const CALENDAR_FEED_FUTURE_DAYS = 180;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Predefined palette for calendar feed colour chips. Keys are stored in the DB;
|
|
16
|
+
* values map to Tailwind bg-* classes used in the calendar view.
|
|
17
|
+
*/
|
|
18
|
+
export const FEED_COLORS = {
|
|
19
|
+
sky: { bg: "bg-sky-600", bgLight: "bg-sky-600/20", border: "border-sky-500/50", text: "text-sky-300", label: "Sky" },
|
|
20
|
+
violet: { bg: "bg-violet-600", bgLight: "bg-violet-600/20", border: "border-violet-500/50", text: "text-violet-300", label: "Violet" },
|
|
21
|
+
emerald: { bg: "bg-emerald-600", bgLight: "bg-emerald-600/20", border: "border-emerald-500/50", text: "text-emerald-300", label: "Emerald" },
|
|
22
|
+
amber: { bg: "bg-amber-600", bgLight: "bg-amber-600/20", border: "border-amber-500/50", text: "text-amber-300", label: "Amber" },
|
|
23
|
+
rose: { bg: "bg-rose-600", bgLight: "bg-rose-600/20", border: "border-rose-500/50", text: "text-rose-300", label: "Rose" },
|
|
24
|
+
cyan: { bg: "bg-cyan-600", bgLight: "bg-cyan-600/20", border: "border-cyan-500/50", text: "text-cyan-300", label: "Cyan" },
|
|
25
|
+
fuchsia: { bg: "bg-fuchsia-600", bgLight: "bg-fuchsia-600/20", border: "border-fuchsia-500/50", text: "text-fuchsia-300", label: "Fuchsia" },
|
|
26
|
+
orange: { bg: "bg-orange-600", bgLight: "bg-orange-600/20", border: "border-orange-500/50", text: "text-orange-300", label: "Orange" },
|
|
27
|
+
} as const;
|
|
28
|
+
|
|
29
|
+
export type FeedColorKey = keyof typeof FEED_COLORS;
|
|
30
|
+
export const FEED_COLOR_KEYS = Object.keys(FEED_COLORS) as FeedColorKey[];
|
|
31
|
+
export const DEFAULT_FEED_COLOR: FeedColorKey = "sky";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const findUnique = vi.fn();
|
|
4
|
+
|
|
5
|
+
vi.mock("@/lib/prisma", () => ({
|
|
6
|
+
prisma: {
|
|
7
|
+
mcpToken: {
|
|
8
|
+
findUnique,
|
|
9
|
+
update: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("jose", () => ({
|
|
15
|
+
jwtVerify: vi.fn().mockResolvedValue({
|
|
16
|
+
payload: { userId: "user-1", workspaceId: "ws-1", tokenId: "tok-1" },
|
|
17
|
+
}),
|
|
18
|
+
SignJWT: class {},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe("verifyMcpToken", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
findUnique.mockReset();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("rejects a token if the workspace does not match the JWT payload", async () => {
|
|
27
|
+
findUnique.mockResolvedValue({
|
|
28
|
+
id: "tok-1",
|
|
29
|
+
userId: "user-1",
|
|
30
|
+
workspaceId: "ws-2",
|
|
31
|
+
revokedAt: null,
|
|
32
|
+
alias: "Laptop",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { verifyMcpToken } = await import("@/lib/mcp-auth");
|
|
36
|
+
|
|
37
|
+
await expect(verifyMcpToken("secret")).resolves.toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accepts a token when the workspace matches the JWT payload", async () => {
|
|
41
|
+
findUnique.mockResolvedValue({
|
|
42
|
+
id: "tok-1",
|
|
43
|
+
userId: "user-1",
|
|
44
|
+
workspaceId: "ws-1",
|
|
45
|
+
revokedAt: null,
|
|
46
|
+
alias: "Laptop",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const { verifyMcpToken } = await import("@/lib/mcp-auth");
|
|
50
|
+
|
|
51
|
+
await expect(verifyMcpToken("secret")).resolves.toEqual({
|
|
52
|
+
userId: "user-1",
|
|
53
|
+
workspaceId: "ws-1",
|
|
54
|
+
tokenId: "tok-1",
|
|
55
|
+
alias: "Laptop",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
package/lib/mcp-auth.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
2
|
+
import { prisma } from "@/lib/prisma";
|
|
3
|
+
|
|
4
|
+
// Throttle lastUsed updates to reduce DB row contention (heartbeat calls ~120/min)
|
|
5
|
+
const THROTTLE_MS = 60_000;
|
|
6
|
+
const lastUsedCache = new Map<string, number>();
|
|
7
|
+
|
|
8
|
+
const MCP_SECRET = new TextEncoder().encode(
|
|
9
|
+
process.env.MCP_SECRET ?? "dev-mcp-secret-change-in-production"
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export type McpTokenPayload = {
|
|
13
|
+
userId: string;
|
|
14
|
+
workspaceId: string;
|
|
15
|
+
tokenId: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function signMcpToken(payload: McpTokenPayload): Promise<string> {
|
|
19
|
+
return new SignJWT({ ...payload })
|
|
20
|
+
.setProtectedHeader({ alg: "HS256" })
|
|
21
|
+
.setIssuedAt()
|
|
22
|
+
.setExpirationTime("1y")
|
|
23
|
+
.sign(MCP_SECRET);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function verifyMcpToken(
|
|
27
|
+
token: string
|
|
28
|
+
): Promise<(McpTokenPayload & { alias: string | null }) | null> {
|
|
29
|
+
try {
|
|
30
|
+
const { payload } = await jwtVerify(token, MCP_SECRET);
|
|
31
|
+
const { userId, workspaceId, tokenId } = payload as McpTokenPayload;
|
|
32
|
+
|
|
33
|
+
// Check DB: not revoked, correct IDs
|
|
34
|
+
const record = await prisma.mcpToken.findUnique({ where: { id: tokenId } });
|
|
35
|
+
if (
|
|
36
|
+
!record ||
|
|
37
|
+
record.revokedAt ||
|
|
38
|
+
record.userId !== userId ||
|
|
39
|
+
record.workspaceId !== workspaceId
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Touch lastUsed (throttled to reduce DB row contention)
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
const lastUpdate = lastUsedCache.get(tokenId) ?? 0;
|
|
47
|
+
if (now - lastUpdate > THROTTLE_MS) {
|
|
48
|
+
lastUsedCache.set(tokenId, now);
|
|
49
|
+
// Fire-and-forget: don't await so it never blocks auth
|
|
50
|
+
prisma.mcpToken.update({
|
|
51
|
+
where: { id: tokenId },
|
|
52
|
+
data: { lastUsed: new Date() },
|
|
53
|
+
}).catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { userId, workspaceId, tokenId, alias: record.alias };
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function extractBearerToken(authHeader: string | null): string | null {
|
|
63
|
+
if (!authHeader?.startsWith("Bearer ")) return null;
|
|
64
|
+
return authHeader.slice(7);
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { SERVER_INSTRUCTIONS, TOOLS } from "@/lib/mcp-contract";
|
|
3
|
+
|
|
4
|
+
describe("MCP contract", () => {
|
|
5
|
+
it("documents markdown equivalents for editor shortcuts", () => {
|
|
6
|
+
expect(SERVER_INSTRUCTIONS).toContain("/todo");
|
|
7
|
+
expect(SERVER_INSTRUCTIONS).toContain("[[Note Title]]");
|
|
8
|
+
expect(SERVER_INSTRUCTIONS).toContain("((Task Title))");
|
|
9
|
+
expect(SERVER_INSTRUCTIONS).toContain("!high");
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("teaches create_note how to write agent-friendly markdown", () => {
|
|
13
|
+
const createNote = TOOLS.find((tool) => tool.name === "create_note");
|
|
14
|
+
|
|
15
|
+
expect(createNote?.description).toContain("editor shortcuts");
|
|
16
|
+
expect(JSON.stringify(createNote?.inputSchema)).toContain("- [ ]");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("teaches append_to_note how to append task-aware markdown", () => {
|
|
20
|
+
const appendToNote = TOOLS.find((tool) => tool.name === "append_to_note");
|
|
21
|
+
|
|
22
|
+
expect(appendToNote?.description).toContain("append markdown");
|
|
23
|
+
expect(JSON.stringify(appendToNote?.inputSchema)).toContain("!high");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
type McpToolSchema = {
|
|
2
|
+
type: "object";
|
|
3
|
+
properties: Record<string, unknown>;
|
|
4
|
+
required: string[];
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type McpTool = {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
inputSchema: McpToolSchema;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const TOOLS: McpTool[] = [
|
|
14
|
+
{
|
|
15
|
+
name: "get_notes",
|
|
16
|
+
description:
|
|
17
|
+
"List all notes in the workspace. Returns note IDs, titles, and task counts. " +
|
|
18
|
+
"Use this first to discover what work is tracked. Note IDs are needed for get_note and append_to_note.",
|
|
19
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "get_tasks",
|
|
23
|
+
description:
|
|
24
|
+
"List unclaimed agent tasks (assigned to @agent, status OPEN, not yet claimed). " +
|
|
25
|
+
"Returns task IDs, titles, source notes, and age. " +
|
|
26
|
+
"Use this to find work items you can pick up. Always claim a task before starting work on it.",
|
|
27
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "get_task_detail",
|
|
31
|
+
description:
|
|
32
|
+
"Get full details of a specific task including its audit log history. " +
|
|
33
|
+
"Use this to understand a task's context, past status changes, and who interacted with it.",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: { taskId: { type: "string", description: "The task ID (from get_tasks or get_notes)" } },
|
|
37
|
+
required: ["taskId"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "get_linked_files",
|
|
42
|
+
description:
|
|
43
|
+
"Get file path references linked to a task (e.g. source files the task relates to). " +
|
|
44
|
+
"Use this to know which files to look at when working on a task.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: { taskId: { type: "string", description: "The task ID" } },
|
|
48
|
+
required: ["taskId"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "claim_tasks",
|
|
53
|
+
description:
|
|
54
|
+
"Claim one or more agent tasks to reserve them for yourself. " +
|
|
55
|
+
"You MUST claim a task before working on it — this prevents other agents from picking it up. " +
|
|
56
|
+
"After claiming, send periodic heartbeats and update the status as you make progress.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
ids: { type: "array", items: { type: "string" }, description: "Task IDs to claim (from get_tasks)" },
|
|
61
|
+
},
|
|
62
|
+
required: ["ids"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "release_task",
|
|
67
|
+
description:
|
|
68
|
+
"Release a task you previously claimed back to the open queue. " +
|
|
69
|
+
"Use this if you cannot complete the task, it's blocked, or it's no longer relevant. " +
|
|
70
|
+
"Provide a reason so the workspace owner understands why it was released. " +
|
|
71
|
+
"This notifies the workspace owner.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
id: { type: "string", description: "The task ID to release" },
|
|
76
|
+
reason: { type: "string", description: "Why you are releasing it (optional but recommended)" },
|
|
77
|
+
},
|
|
78
|
+
required: ["id"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "update_task",
|
|
83
|
+
description:
|
|
84
|
+
"Update the status of a task you have claimed. Status flow: in_progress → review → done. " +
|
|
85
|
+
"Use 'in_progress' when you start working, 'review' when work is complete and awaiting approval, " +
|
|
86
|
+
"'done' when fully finished (this also auto-checks the task off in the source note). " +
|
|
87
|
+
"Each update resets the heartbeat timer.",
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
id: { type: "string", description: "The task ID" },
|
|
92
|
+
status: {
|
|
93
|
+
type: "string",
|
|
94
|
+
enum: ["in_progress", "review", "done"],
|
|
95
|
+
description: "New status: in_progress (working), review (awaiting approval), done (completed)",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
required: ["id", "status"],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "heartbeat",
|
|
103
|
+
description:
|
|
104
|
+
"Send a heartbeat to signal you are still actively working on a claimed task. " +
|
|
105
|
+
"Send heartbeats regularly (e.g. every few minutes) while working on long tasks " +
|
|
106
|
+
"so the system knows the task hasn't been abandoned.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: { id: { type: "string", description: "The task ID" } },
|
|
110
|
+
required: ["id"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "create_note",
|
|
115
|
+
description:
|
|
116
|
+
"Create a new note in the workspace with markdown content. Use markdown, not literal editor shortcuts: " +
|
|
117
|
+
"/todo becomes '- [ ] Task title', + note becomes '[[Note Title]]', + task becomes '((Task Title))', " +
|
|
118
|
+
"+ mention becomes '@agent' or '@human(name)', and + priority becomes '!high' or another !priority tag. " +
|
|
119
|
+
"Checkbox task lines automatically create tasks and parse priority and ISO date metadata.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
title: { type: "string", description: "Note title (e.g. 'Sprint 24 Tasks')" },
|
|
124
|
+
content: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description:
|
|
127
|
+
"Markdown content using the parser's real syntax. Example task: '- [ ] Ship MCP docs @agent !high " +
|
|
128
|
+
"2026-06-14..2026-06-20 <app/mcp/route.tsx>'. Use '- [x]' for completed tasks, '[[Note Title]]' " +
|
|
129
|
+
"for note links, '((Task Title))' for task references, and @human(name) for human-assigned tasks.",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ["title"],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "get_note",
|
|
137
|
+
description:
|
|
138
|
+
"Get a note's full decrypted markdown content along with all its tasks. " +
|
|
139
|
+
"Use this to read the complete context of a task or to see all work tracked in a note.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: { noteId: { type: "string", description: "The note ID (from get_notes)" } },
|
|
143
|
+
required: ["noteId"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "append_to_note",
|
|
148
|
+
description:
|
|
149
|
+
"Append markdown content to an existing note. Use append markdown with the same note-writing syntax as create_note: " +
|
|
150
|
+
"checkbox tasks, note links, task references, mentions, !priority tags, and ISO dates are all supported. " +
|
|
151
|
+
"New task lines automatically create tasks without overwriting existing content. Cannot be used on locked notes.",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
noteId: { type: "string", description: "The note ID (from get_notes)" },
|
|
156
|
+
content: {
|
|
157
|
+
type: "string",
|
|
158
|
+
description:
|
|
159
|
+
"Markdown to append, such as '- [ ] Review release notes @agent !high 2026-06-14 <docs/release.md>' " +
|
|
160
|
+
"or 'Linked note [[Release Plan]] with task ref ((Review release notes))'.",
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
required: ["noteId", "content"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
export const SERVER_INSTRUCTIONS = [
|
|
169
|
+
"You are connected to Knotpad, a task and note management workspace for dev teams.",
|
|
170
|
+
"",
|
|
171
|
+
"## Core Concepts",
|
|
172
|
+
"- **Notes** contain markdown with embedded task checkboxes.",
|
|
173
|
+
"- **Tasks** are parsed from notes and can be assigned to @agent (AI) or @human.",
|
|
174
|
+
"- Agent tasks must be **claimed** before working on them to avoid conflicts.",
|
|
175
|
+
"- Tasks have a lifecycle: OPEN → CLAIMED → IN_PROGRESS → REVIEW → DONE.",
|
|
176
|
+
"",
|
|
177
|
+
"## Recommended Agent Workflow",
|
|
178
|
+
"1. Call `get_notes` to see all notes and identify work areas.",
|
|
179
|
+
"2. Call `get_tasks` to see unclaimed agent tasks available to you.",
|
|
180
|
+
"3. Call `get_task_detail` and `get_linked_files` to understand a task's context and related files.",
|
|
181
|
+
"4. Call `claim_tasks` to reserve the tasks you want to work on.",
|
|
182
|
+
"5. Call `update_task` with status 'in_progress' when you start.",
|
|
183
|
+
"6. Send `heartbeat` periodically while working on long tasks.",
|
|
184
|
+
"7. When work is done, call `update_task` with status 'review' or 'done'.",
|
|
185
|
+
"8. Use `create_note` or `append_to_note` to document your work or create new task plans.",
|
|
186
|
+
"",
|
|
187
|
+
"## Writing Notes Correctly",
|
|
188
|
+
"- Prefer valid markdown content over UI-specific wording like 'press /todo'.",
|
|
189
|
+
"- `/todo` in the editor becomes `- [ ] Task title` in note content.",
|
|
190
|
+
"- `/bullet` in the editor becomes `- item` in note content.",
|
|
191
|
+
"- `+ note` becomes `[[Note Title]]`.",
|
|
192
|
+
"- `+ task` becomes `((Task Title))` for referencing an existing task.",
|
|
193
|
+
"- `+ mention` becomes `@agent` or `@human(name)`.",
|
|
194
|
+
"- `+ priority` becomes `!critical`, `!high`, `!medium`, or `!low`.",
|
|
195
|
+
"- Supported date syntax is `YYYY-MM-DD` or `YYYY-MM-DD..YYYY-MM-DD`.",
|
|
196
|
+
"",
|
|
197
|
+
"## Task Syntax in Notes",
|
|
198
|
+
"- Agent task: `- [ ] Fix login bug @agent <src/auth/login.ts> !high 2026-06-14`",
|
|
199
|
+
"- Completed task: `- [x] Ship MCP docs @agent !high 2026-06-14..2026-06-20 <app/mcp/route.tsx>`",
|
|
200
|
+
"- Human task: `- [ ] Review design @human(alice)`",
|
|
201
|
+
"- Note link: `[[Sprint Plan]]`",
|
|
202
|
+
"- Task reference: `((Fix login bug))` anywhere in note text.",
|
|
203
|
+
"",
|
|
204
|
+
"## Important Rules",
|
|
205
|
+
"- Always claim a task before starting work on it.",
|
|
206
|
+
"- Send heartbeats every few minutes for long-running tasks.",
|
|
207
|
+
"- Release tasks you cannot complete, with a reason.",
|
|
208
|
+
"- Only update status on tasks you have claimed.",
|
|
209
|
+
"- When marking a task 'done', the source note checkbox is automatically checked.",
|
|
210
|
+
].join("\n");
|