@telvok/librarian-mcp 1.5.4 → 2.0.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.
Files changed (119) hide show
  1. package/dist/library/errors.d.ts +48 -0
  2. package/dist/library/errors.js +80 -0
  3. package/dist/library/schemas.d.ts +6 -6
  4. package/dist/library/storage.d.ts +2 -2
  5. package/dist/library/storage.js +2 -2
  6. package/dist/library 2/embeddings.d.ts +21 -0
  7. package/dist/library 2/embeddings.js +86 -0
  8. package/dist/library 2/manager.d.ts +42 -0
  9. package/dist/library 2/manager.js +218 -0
  10. package/dist/library 2/parsers/cursor.d.ts +15 -0
  11. package/dist/library 2/parsers/cursor.js +168 -0
  12. package/dist/library 2/parsers/index.d.ts +6 -0
  13. package/dist/library 2/parsers/index.js +5 -0
  14. package/dist/library 2/parsers/json.d.ts +11 -0
  15. package/dist/library 2/parsers/json.js +95 -0
  16. package/dist/library 2/parsers/jsonl.d.ts +14 -0
  17. package/dist/library 2/parsers/jsonl.js +85 -0
  18. package/dist/library 2/parsers/markdown.d.ts +15 -0
  19. package/dist/library 2/parsers/markdown.js +77 -0
  20. package/dist/library 2/parsers/sqlite.d.ts +8 -0
  21. package/dist/library 2/parsers/sqlite.js +123 -0
  22. package/dist/library 2/parsers/types.d.ts +21 -0
  23. package/dist/library 2/parsers/types.js +4 -0
  24. package/dist/library 2/query.d.ts +26 -0
  25. package/dist/library 2/query.js +104 -0
  26. package/dist/library 2/schemas.d.ts +324 -0
  27. package/dist/library 2/schemas.js +79 -0
  28. package/dist/library 2/storage.d.ts +22 -0
  29. package/dist/library 2/storage.js +36 -0
  30. package/dist/library 2/vector-index.d.ts +55 -0
  31. package/dist/library 2/vector-index.js +160 -0
  32. package/dist/server 2.js +199 -0
  33. package/dist/server.d 2.ts +2 -0
  34. package/dist/server.js +102 -54
  35. package/dist/tools/adopt.d.ts +1 -0
  36. package/dist/tools/adopt.js +37 -10
  37. package/dist/tools/auth.d.ts +69 -0
  38. package/dist/tools/auth.js +379 -0
  39. package/dist/tools/bounty-claim.d.ts +28 -0
  40. package/dist/tools/bounty-claim.js +92 -0
  41. package/dist/tools/bounty-create.d.ts +47 -0
  42. package/dist/tools/bounty-create.js +118 -0
  43. package/dist/tools/bounty-list.d.ts +50 -0
  44. package/dist/tools/bounty-list.js +116 -0
  45. package/dist/tools/bounty-submit.d.ts +34 -0
  46. package/dist/tools/bounty-submit.js +94 -0
  47. package/dist/tools/brief.d.ts +94 -0
  48. package/dist/tools/brief.js +234 -15
  49. package/dist/tools/delete.d.ts +87 -0
  50. package/dist/tools/delete.js +266 -0
  51. package/dist/tools/feedback.d.ts +27 -0
  52. package/dist/tools/feedback.js +98 -0
  53. package/dist/tools/help.d.ts +22 -0
  54. package/dist/tools/help.js +482 -0
  55. package/dist/tools/import-memories.d.ts +1 -0
  56. package/dist/tools/import-memories.js +18 -13
  57. package/dist/tools/index.d.ts +11 -0
  58. package/dist/tools/index.js +12 -0
  59. package/dist/tools/library-buy.d.ts +31 -0
  60. package/dist/tools/library-buy.js +104 -0
  61. package/dist/tools/library-download.d.ts +27 -0
  62. package/dist/tools/library-download.js +177 -0
  63. package/dist/tools/library-publish.d.ts +112 -0
  64. package/dist/tools/library-publish.js +387 -0
  65. package/dist/tools/library-search.d.ts +110 -0
  66. package/dist/tools/library-search.js +132 -0
  67. package/dist/tools/mark-hit.d.ts +1 -0
  68. package/dist/tools/mark-hit.js +83 -5
  69. package/dist/tools/my-books.d.ts +51 -0
  70. package/dist/tools/my-books.js +115 -0
  71. package/dist/tools/my-bounties.d.ts +43 -0
  72. package/dist/tools/my-bounties.js +126 -0
  73. package/dist/tools/rate-book.d.ts +40 -0
  74. package/dist/tools/rate-book.js +147 -0
  75. package/dist/tools/rebuild-index.d.ts +1 -0
  76. package/dist/tools/rebuild-index.js +40 -8
  77. package/dist/tools/record.d.ts +18 -0
  78. package/dist/tools/record.js +30 -26
  79. package/dist/tools/seller-analytics.d.ts +53 -0
  80. package/dist/tools/seller-analytics.js +180 -0
  81. package/dist/tools/sync.d.ts +55 -0
  82. package/dist/tools/sync.js +304 -0
  83. package/dist/tools/unsubscribe.d.ts +48 -0
  84. package/dist/tools/unsubscribe.js +120 -0
  85. package/dist/tools 2/adopt.d.ts +24 -0
  86. package/dist/tools 2/adopt.js +154 -0
  87. package/dist/tools 2/auth.d.ts +35 -0
  88. package/dist/tools 2/auth.js +229 -0
  89. package/dist/tools 2/brief.d.ts +56 -0
  90. package/dist/tools 2/brief.js +414 -0
  91. package/dist/tools 2/help.d.ts +21 -0
  92. package/dist/tools 2/help.js +267 -0
  93. package/dist/tools 2/import-memories.d.ts +32 -0
  94. package/dist/tools 2/import-memories.js +231 -0
  95. package/dist/tools 2/index.d.ts +12 -0
  96. package/dist/tools 2/index.js +12 -0
  97. package/dist/tools 2/mark-hit.d.ts +20 -0
  98. package/dist/tools 2/mark-hit.js +71 -0
  99. package/dist/tools 2/marketplace-buy.d.ts +30 -0
  100. package/dist/tools 2/marketplace-buy.js +97 -0
  101. package/dist/tools 2/marketplace-download.d.ts +26 -0
  102. package/dist/tools 2/marketplace-download.js +160 -0
  103. package/dist/tools 2/marketplace-publish.d.ts +111 -0
  104. package/dist/tools 2/marketplace-publish.js +377 -0
  105. package/dist/tools 2/marketplace-search.d.ts +57 -0
  106. package/dist/tools 2/marketplace-search.js +96 -0
  107. package/dist/tools 2/my-books.d.ts +50 -0
  108. package/dist/tools 2/my-books.js +107 -0
  109. package/dist/tools 2/rate-book.d.ts +39 -0
  110. package/dist/tools 2/rate-book.js +139 -0
  111. package/dist/tools 2/rebuild-index.d.ts +23 -0
  112. package/dist/tools 2/rebuild-index.js +107 -0
  113. package/dist/tools 2/record.d.ts +40 -0
  114. package/dist/tools 2/record.js +205 -0
  115. package/dist/tools 2/seller-analytics.d.ts +35 -0
  116. package/dist/tools 2/seller-analytics.js +102 -0
  117. package/dist/tools 2/sync.d.ts +54 -0
  118. package/dist/tools 2/sync.js +298 -0
  119. package/package.json +1 -1
@@ -0,0 +1,304 @@
1
+ // ============================================================================
2
+ // Sync Tool
3
+ // Check for and receive updates to owned books from Telvok library
4
+ // ============================================================================
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import { loadApiKey } from './auth.js';
8
+ import { getLibraryPath, getImportedPath } from '../library/storage.js';
9
+ const TELVOK_API_URL = process.env.TELVOK_API_URL || 'https://telvok.com';
10
+ // ============================================================================
11
+ // Tool Definition
12
+ // ============================================================================
13
+ export const syncTool = {
14
+ name: 'sync',
15
+ title: 'Sync Purchased Books',
16
+ description: `Check for and receive updates to purchased books.
17
+
18
+ USE THIS TOOL WHEN:
19
+ - User asks to update/sync their purchased books
20
+ - Starting a session and marketplace content might have changed
21
+ - Checking if owned books have new entries
22
+
23
+ Subscription books sync automatically. One-time purchases sync on request.
24
+
25
+ TRIGGER PATTERNS:
26
+ - "Update my books" → sync()
27
+ - "Check for new content" → sync()
28
+ - "Sync that book" → sync({ slug: "book-slug" })
29
+ - Force sync manual books → sync({ options: { force: true } })
30
+
31
+ Examples:
32
+ - sync() - Check and sync all auto-sync books
33
+ - sync({ slug: "premium-patterns" }) - Sync specific book
34
+ - sync({ options: { force: true } }) - Include manual preference books`,
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ slug: {
39
+ type: 'string',
40
+ description: 'Specific book slug to sync (omit for all)',
41
+ },
42
+ options: {
43
+ type: 'object',
44
+ properties: {
45
+ force: {
46
+ type: 'boolean',
47
+ description: 'Include manual preference books (default: false)',
48
+ },
49
+ download: {
50
+ type: 'boolean',
51
+ description: 'Download open book updates to local library (default: false)',
52
+ },
53
+ },
54
+ description: 'Sync options',
55
+ },
56
+ },
57
+ required: [],
58
+ },
59
+ async handler(args) {
60
+ const { slug, options } = (args || {});
61
+ const force = options?.force || false;
62
+ const download = options?.download || false;
63
+ // Check authentication
64
+ const apiKey = await loadApiKey();
65
+ if (!apiKey) {
66
+ return {
67
+ success: false,
68
+ message: 'Not authenticated. Run auth({ action: "login" }) to connect your Telvok account first.',
69
+ };
70
+ }
71
+ try {
72
+ // Step 1: Check for available updates
73
+ const checkUrl = new URL(`${TELVOK_API_URL}/api/sync`);
74
+ if (slug) {
75
+ checkUrl.searchParams.set('slug', slug);
76
+ }
77
+ const checkResponse = await fetch(checkUrl.toString(), {
78
+ method: 'GET',
79
+ headers: {
80
+ 'Authorization': `Bearer ${apiKey}`,
81
+ },
82
+ });
83
+ const checkData = await checkResponse.json();
84
+ if (!checkResponse.ok) {
85
+ return {
86
+ success: false,
87
+ message: checkData.error || `Failed to check for updates: HTTP ${checkResponse.status}`,
88
+ };
89
+ }
90
+ // If no updates available
91
+ const updatesAvailable = checkData.updates_available || [];
92
+ const upToDate = checkData.up_to_date || [];
93
+ const pinned = checkData.pinned || [];
94
+ if (updatesAvailable.length === 0) {
95
+ const totalBooks = upToDate.length + pinned.length;
96
+ let message = '';
97
+ if (totalBooks === 0) {
98
+ message = 'No books to sync. Purchase or claim books first.';
99
+ }
100
+ else if (pinned.length > 0) {
101
+ message = `${upToDate.length} book${upToDate.length === 1 ? ' is' : 's are'} up to date. ${pinned.length} pinned (won't sync).`;
102
+ }
103
+ else {
104
+ message = `All ${totalBooks} book${totalBooks === 1 ? ' is' : 's are'} up to date.`;
105
+ }
106
+ return {
107
+ success: true,
108
+ message,
109
+ pinned: pinned.length > 0 ? pinned.map((p) => ({ slug: p.slug, name: p.name })) : undefined,
110
+ up_to_date: upToDate.length,
111
+ };
112
+ }
113
+ // Step 2: Perform sync
114
+ const syncResponse = await fetch(`${TELVOK_API_URL}/api/sync`, {
115
+ method: 'POST',
116
+ headers: {
117
+ 'Authorization': `Bearer ${apiKey}`,
118
+ 'Content-Type': 'application/json',
119
+ },
120
+ body: JSON.stringify({
121
+ slug,
122
+ force,
123
+ include_content: download,
124
+ }),
125
+ });
126
+ const syncData = await syncResponse.json();
127
+ if (!syncResponse.ok) {
128
+ return {
129
+ success: false,
130
+ message: syncData.error || `Failed to sync: HTTP ${syncResponse.status}`,
131
+ };
132
+ }
133
+ const synced = [];
134
+ const available = [];
135
+ const pinnedBooks = [];
136
+ // Process synced books
137
+ for (const book of syncData.synced || []) {
138
+ const newCount = typeof book.new_entries === 'number' ? book.new_entries : book.new_entries?.length || 0;
139
+ const modifiedCount = typeof book.modified_entries === 'number' ? book.modified_entries : book.modified_entries?.length || 0;
140
+ // If download requested and content provided, save to local
141
+ if (download && book.access_method === 'download' && Array.isArray(book.new_entries)) {
142
+ await saveEntriesToLocal(book.slug, book.new_entries, book.modified_entries);
143
+ }
144
+ synced.push({
145
+ slug: book.slug,
146
+ name: book.name,
147
+ new_entries: newCount,
148
+ modified_entries: modifiedCount,
149
+ access: download && book.pricing_type === 'open' ? 'downloaded' : 'cloud',
150
+ version: formatVersion(book.new_version),
151
+ synced_from: formatVersion(book.previous_version),
152
+ });
153
+ }
154
+ // Process skipped books
155
+ for (const book of syncData.skipped || []) {
156
+ if (book.reason === 'manual_no_force') {
157
+ const update = updatesAvailable.find((u) => u.slug === book.slug);
158
+ if (update) {
159
+ available.push({
160
+ slug: book.slug,
161
+ name: book.name,
162
+ new_entries: update.new_entries_count,
163
+ });
164
+ }
165
+ }
166
+ else if (book.reason === 'pinned') {
167
+ pinnedBooks.push({
168
+ slug: book.slug,
169
+ name: book.name,
170
+ });
171
+ }
172
+ }
173
+ // Build summary message
174
+ const messageParts = [];
175
+ const totalNewEntries = synced.reduce((sum, b) => sum + b.new_entries, 0);
176
+ const totalModified = synced.reduce((sum, b) => sum + b.modified_entries, 0);
177
+ if (synced.length > 0) {
178
+ let syncMsg = `Synced ${synced.length} book${synced.length === 1 ? '' : 's'}`;
179
+ if (totalNewEntries > 0 || totalModified > 0) {
180
+ const parts = [];
181
+ if (totalNewEntries > 0)
182
+ parts.push(`${totalNewEntries} new`);
183
+ if (totalModified > 0)
184
+ parts.push(`${totalModified} modified`);
185
+ syncMsg += `: ${parts.join(', ')} entries.`;
186
+ }
187
+ else {
188
+ syncMsg += '.';
189
+ }
190
+ messageParts.push(syncMsg);
191
+ }
192
+ if (available.length > 0) {
193
+ messageParts.push(`${available.length} book${available.length === 1 ? ' has' : 's have'} updates available (set to manual).`);
194
+ }
195
+ if (pinnedBooks.length > 0) {
196
+ messageParts.push(`${pinnedBooks.length} book${pinnedBooks.length === 1 ? '' : 's'} pinned (won't sync).`);
197
+ }
198
+ if (messageParts.length === 0) {
199
+ if (upToDate.length > 0) {
200
+ messageParts.push(`All ${upToDate.length} book${upToDate.length === 1 ? ' is' : 's are'} up to date.`);
201
+ }
202
+ else {
203
+ messageParts.push('No books to sync. Purchase or claim books first.');
204
+ }
205
+ }
206
+ return {
207
+ success: true,
208
+ message: messageParts.join(' '),
209
+ synced: synced.length > 0 ? synced : undefined,
210
+ available: available.length > 0 ? available : undefined,
211
+ pinned: pinnedBooks.length > 0 ? pinnedBooks : undefined,
212
+ up_to_date: upToDate.length,
213
+ };
214
+ }
215
+ catch (error) {
216
+ const message = error instanceof Error ? error.message : String(error);
217
+ throw new Error(`Sync failed: ${message}`);
218
+ }
219
+ },
220
+ };
221
+ // ============================================================================
222
+ // Helper: Save entries to local imported folder
223
+ // ============================================================================
224
+ async function saveEntriesToLocal(slug, newEntries, modifiedEntries) {
225
+ const libraryPath = getLibraryPath();
226
+ const importedPath = getImportedPath(libraryPath);
227
+ const bookPath = path.join(importedPath, slug);
228
+ // Ensure directory exists
229
+ await fs.mkdir(bookPath, { recursive: true });
230
+ // Save new entries
231
+ for (const entry of newEntries || []) {
232
+ const filename = slugify(entry.title) + '.md';
233
+ const content = formatEntryAsMarkdown(entry);
234
+ await fs.writeFile(path.join(bookPath, filename), content, 'utf-8');
235
+ }
236
+ // Save modified entries (overwrite)
237
+ for (const entry of modifiedEntries || []) {
238
+ const filename = slugify(entry.title) + '.md';
239
+ const content = formatEntryAsMarkdown(entry);
240
+ await fs.writeFile(path.join(bookPath, filename), content, 'utf-8');
241
+ }
242
+ // Update .meta.json
243
+ const metaPath = path.join(bookPath, '.meta.json');
244
+ let meta = {};
245
+ try {
246
+ const existing = await fs.readFile(metaPath, 'utf-8');
247
+ meta = JSON.parse(existing);
248
+ }
249
+ catch {
250
+ // No existing meta
251
+ }
252
+ meta.last_synced = new Date().toISOString();
253
+ await fs.writeFile(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
254
+ }
255
+ function slugify(text) {
256
+ return text
257
+ .toLowerCase()
258
+ .replace(/[^a-z0-9]+/g, '-')
259
+ .replace(/^-|-$/g, '')
260
+ .substring(0, 50);
261
+ }
262
+ /**
263
+ * Format a timestamp as a human-readable version string
264
+ */
265
+ function formatVersion(isoTimestamp) {
266
+ if (!isoTimestamp)
267
+ return 'never synced';
268
+ const date = new Date(isoTimestamp);
269
+ const now = new Date();
270
+ const diffMs = now.getTime() - date.getTime();
271
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
272
+ if (diffDays === 0) {
273
+ return `today at ${date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })}`;
274
+ }
275
+ else if (diffDays === 1) {
276
+ return 'yesterday';
277
+ }
278
+ else if (diffDays < 7) {
279
+ return `${diffDays} days ago`;
280
+ }
281
+ else {
282
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
283
+ }
284
+ }
285
+ function formatEntryAsMarkdown(entry) {
286
+ let md = '';
287
+ // Frontmatter
288
+ if (entry.intent || entry.context) {
289
+ md += '---\n';
290
+ if (entry.intent)
291
+ md += `intent: "${entry.intent}"\n`;
292
+ if (entry.context)
293
+ md += `context: "${entry.context}"\n`;
294
+ md += '---\n\n';
295
+ }
296
+ // Title and content
297
+ md += `# ${entry.title}\n\n`;
298
+ md += entry.content;
299
+ return md;
300
+ }
301
+ // ============================================================================
302
+ // Export
303
+ // ============================================================================
304
+ export default syncTool;
@@ -0,0 +1,48 @@
1
+ interface UnsubscribeResult {
2
+ success: boolean;
3
+ message: string;
4
+ book?: {
5
+ slug: string;
6
+ name: string;
7
+ };
8
+ }
9
+ export declare const unsubscribeTool: {
10
+ name: string;
11
+ title: string;
12
+ description: string;
13
+ inputSchema: {
14
+ type: "object";
15
+ properties: {
16
+ slug: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ };
21
+ required: string[];
22
+ };
23
+ outputSchema: {
24
+ type: "object";
25
+ properties: {
26
+ success: {
27
+ type: string;
28
+ };
29
+ message: {
30
+ type: string;
31
+ };
32
+ book: {
33
+ type: string;
34
+ properties: {
35
+ slug: {
36
+ type: string;
37
+ };
38
+ name: {
39
+ type: string;
40
+ };
41
+ };
42
+ };
43
+ };
44
+ required: string[];
45
+ };
46
+ handler(args: unknown): Promise<UnsubscribeResult>;
47
+ };
48
+ export default unsubscribeTool;
@@ -0,0 +1,120 @@
1
+ // ============================================================================
2
+ // Unsubscribe Tool
3
+ // Cancel a subscription to a book from Telvok library
4
+ // ============================================================================
5
+ import { loadApiKey } from './auth.js';
6
+ const TELVOK_API_URL = process.env.TELVOK_API_URL || 'https://telvok.com';
7
+ // ============================================================================
8
+ // Tool Definition
9
+ // ============================================================================
10
+ export const unsubscribeTool = {
11
+ name: 'unsubscribe',
12
+ title: 'Cancel Subscription',
13
+ description: `Cancel a subscription to a book.
14
+
15
+ USE THIS TOOL WHEN:
16
+ - User wants to stop a subscription
17
+ - User says "unsubscribe", "cancel subscription", "stop paying for X"
18
+ - User asks to cancel recurring payment for a book
19
+
20
+ Only works for subscription purchases. One-time purchases grant permanent access.
21
+
22
+ TRIGGER PATTERNS:
23
+ - "Cancel my subscription to X" → unsubscribe({ slug: "book-slug" })
24
+ - "Unsubscribe from that book" → unsubscribe({ slug: "..." })
25
+ - "Stop my subscription" → First use my_books() to find subscription slugs
26
+
27
+ Example:
28
+ - unsubscribe({ slug: "premium-patterns" })`,
29
+ inputSchema: {
30
+ type: 'object',
31
+ properties: {
32
+ slug: {
33
+ type: 'string',
34
+ description: 'Book slug to unsubscribe from (from my_books() results)',
35
+ },
36
+ },
37
+ required: ['slug'],
38
+ },
39
+ outputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ success: { type: 'boolean' },
43
+ message: { type: 'string' },
44
+ book: {
45
+ type: 'object',
46
+ properties: {
47
+ slug: { type: 'string' },
48
+ name: { type: 'string' },
49
+ },
50
+ },
51
+ },
52
+ required: ['success', 'message'],
53
+ },
54
+ async handler(args) {
55
+ const { slug } = args;
56
+ if (!slug || typeof slug !== 'string') {
57
+ return {
58
+ success: false,
59
+ message: 'Book slug is required. Use my_books() to find your subscriptions.',
60
+ };
61
+ }
62
+ // Check authentication
63
+ const apiKey = await loadApiKey();
64
+ if (!apiKey) {
65
+ return {
66
+ success: false,
67
+ message: 'Not authenticated. Run auth({ action: "login" }) to connect your Telvok account first.',
68
+ };
69
+ }
70
+ try {
71
+ const response = await fetch(`${TELVOK_API_URL}/api/subscription/cancel`, {
72
+ method: 'POST',
73
+ headers: {
74
+ 'Authorization': `Bearer ${apiKey}`,
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ body: JSON.stringify({ slug }),
78
+ });
79
+ const data = await response.json();
80
+ if (!response.ok) {
81
+ // Handle specific error cases
82
+ if (response.status === 404) {
83
+ if (data.error?.includes('No active subscription')) {
84
+ return {
85
+ success: false,
86
+ message: `No active subscription found for "${slug}". Use my_books() to see your current subscriptions.`,
87
+ };
88
+ }
89
+ return {
90
+ success: false,
91
+ message: `Book "${slug}" not found.`,
92
+ };
93
+ }
94
+ if (response.status === 400 && data.error?.includes('not a subscription')) {
95
+ return {
96
+ success: false,
97
+ message: data.message || 'This is a one-time purchase. You retain permanent access - no subscription to cancel.',
98
+ };
99
+ }
100
+ return {
101
+ success: false,
102
+ message: data.error || `Failed to unsubscribe: HTTP ${response.status}`,
103
+ };
104
+ }
105
+ return {
106
+ success: true,
107
+ message: data.message || `Subscription to "${data.book?.name || slug}" has been cancelled.`,
108
+ book: data.book,
109
+ };
110
+ }
111
+ catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ throw new Error(`Failed to unsubscribe: ${message}`);
114
+ }
115
+ },
116
+ };
117
+ // ============================================================================
118
+ // Export
119
+ // ============================================================================
120
+ export default unsubscribeTool;
@@ -0,0 +1,24 @@
1
+ export interface AdoptResult {
2
+ success: boolean;
3
+ from: string;
4
+ to: string;
5
+ }
6
+ export declare const adoptTool: {
7
+ name: string;
8
+ description: string;
9
+ inputSchema: {
10
+ type: "object";
11
+ properties: {
12
+ path: {
13
+ type: string;
14
+ description: string;
15
+ };
16
+ title: {
17
+ type: string;
18
+ description: string;
19
+ };
20
+ };
21
+ required: string[];
22
+ };
23
+ handler(args: unknown): Promise<AdoptResult>;
24
+ };
@@ -0,0 +1,154 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { getLibraryPath, getImportedPath, getLocalPath, getPackagesPath } from '../library/storage.js';
5
+ // ============================================================================
6
+ // Tool Definition
7
+ // ============================================================================
8
+ export const adoptTool = {
9
+ name: 'adopt',
10
+ description: `Make imported knowledge ours.
11
+
12
+ When an entry from an imported package proves useful, adopt it into our
13
+ local library. It graduates from "their knowledge" to "our knowledge" -
14
+ now we can edit and evolve it.
15
+
16
+ Examples:
17
+ - adopt({ path: "imported/stripe-patterns/webhook-idempotency" })
18
+ - adopt({ path: "imported/auth-patterns/token-refresh", title: "Our token refresh" })`,
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ path: {
23
+ type: 'string',
24
+ description: "Path to entry to adopt (e.g., 'imported/package-name/entry-name')",
25
+ },
26
+ title: {
27
+ type: 'string',
28
+ description: 'New title for adopted entry. Keeps original if not provided.',
29
+ },
30
+ },
31
+ required: ['path'],
32
+ },
33
+ async handler(args) {
34
+ const { path: entryPath, title: newTitle } = args;
35
+ if (!entryPath) {
36
+ throw new Error('path is required');
37
+ }
38
+ const libraryPath = getLibraryPath();
39
+ const importedPath = getImportedPath(libraryPath);
40
+ const packagesPath = getPackagesPath(libraryPath);
41
+ const localPath = getLocalPath(libraryPath);
42
+ // Normalize path: strip "imported/" or "packages/" prefix if present, add .md if missing
43
+ let normalizedPath = entryPath;
44
+ if (normalizedPath.startsWith('imported/')) {
45
+ normalizedPath = normalizedPath.slice('imported/'.length);
46
+ }
47
+ else if (normalizedPath.startsWith('packages/')) {
48
+ normalizedPath = normalizedPath.slice('packages/'.length);
49
+ }
50
+ if (!normalizedPath.endsWith('.md')) {
51
+ normalizedPath += '.md';
52
+ }
53
+ // Try both imported/ and packages/ paths
54
+ let sourcePath = path.join(importedPath, normalizedPath);
55
+ try {
56
+ await fs.access(sourcePath);
57
+ }
58
+ catch {
59
+ // Try packages path (for marketplace downloads)
60
+ sourcePath = path.join(packagesPath, normalizedPath);
61
+ try {
62
+ await fs.access(sourcePath);
63
+ }
64
+ catch {
65
+ throw new Error(`Entry not found: ${entryPath}`);
66
+ }
67
+ }
68
+ // Read source file
69
+ const content = await fs.readFile(sourcePath, 'utf-8');
70
+ const { data, content: body } = matter(content);
71
+ // Extract package name from path
72
+ const pathParts = normalizedPath.split('/');
73
+ const packageName = pathParts[0];
74
+ // Determine title
75
+ let title = newTitle;
76
+ if (!title) {
77
+ // Try to extract from frontmatter or H1
78
+ title = data.title;
79
+ if (!title) {
80
+ const headingMatch = body.match(/^#\s+(.+)$/m);
81
+ if (headingMatch) {
82
+ title = headingMatch[1].trim();
83
+ }
84
+ else {
85
+ title = path.basename(sourcePath, '.md').replace(/-/g, ' ');
86
+ }
87
+ }
88
+ }
89
+ // Generate slug for new filename
90
+ const slug = slugify(title);
91
+ const now = new Date().toISOString();
92
+ // Ensure local directory exists
93
+ await fs.mkdir(localPath, { recursive: true });
94
+ // Handle filename collisions
95
+ let filename = `${slug}.md`;
96
+ let destPath = path.join(localPath, filename);
97
+ let counter = 1;
98
+ while (await fileExists(destPath)) {
99
+ filename = `${slug}-${counter}.md`;
100
+ destPath = path.join(localPath, filename);
101
+ counter++;
102
+ }
103
+ // Build new frontmatter
104
+ const newFrontmatter = {
105
+ ...data,
106
+ updated: now,
107
+ source: `adopted from ${packageName}`,
108
+ };
109
+ // Update title in frontmatter if changed
110
+ if (newTitle) {
111
+ newFrontmatter.title = newTitle;
112
+ }
113
+ // Update body if title changed
114
+ let newBody = body;
115
+ if (newTitle) {
116
+ // Replace first H1 if exists
117
+ const headingMatch = body.match(/^#\s+.+$/m);
118
+ if (headingMatch) {
119
+ newBody = body.replace(/^#\s+.+$/m, `# ${newTitle}`);
120
+ }
121
+ else {
122
+ // Prepend title
123
+ newBody = `# ${newTitle}\n\n${body}`;
124
+ }
125
+ }
126
+ // Write adopted file
127
+ const fileContent = matter.stringify(newBody, newFrontmatter);
128
+ await fs.writeFile(destPath, fileContent, 'utf-8');
129
+ return {
130
+ success: true,
131
+ from: path.relative(libraryPath, sourcePath),
132
+ to: path.relative(libraryPath, destPath),
133
+ };
134
+ },
135
+ };
136
+ // ============================================================================
137
+ // Helper Functions
138
+ // ============================================================================
139
+ function slugify(text) {
140
+ return text
141
+ .toLowerCase()
142
+ .replace(/[^a-z0-9]+/g, '-')
143
+ .replace(/^-+|-+$/g, '')
144
+ .slice(0, 50);
145
+ }
146
+ async function fileExists(filePath) {
147
+ try {
148
+ await fs.access(filePath);
149
+ return true;
150
+ }
151
+ catch {
152
+ return false;
153
+ }
154
+ }
@@ -0,0 +1,35 @@
1
+ export interface AuthData {
2
+ api_key: string;
3
+ user_email: string;
4
+ user_id: string;
5
+ created_at: string;
6
+ }
7
+ export interface AuthResult {
8
+ authenticated: boolean;
9
+ user_email?: string;
10
+ user_id?: string;
11
+ message: string;
12
+ verification_url?: string;
13
+ user_code?: string;
14
+ }
15
+ export declare const authTool: {
16
+ name: string;
17
+ description: string;
18
+ inputSchema: {
19
+ type: "object";
20
+ properties: {
21
+ action: {
22
+ type: string;
23
+ enum: string[];
24
+ description: string;
25
+ };
26
+ };
27
+ required: string[];
28
+ };
29
+ handler(args: unknown): Promise<AuthResult>;
30
+ };
31
+ /**
32
+ * Load saved API key from auth file
33
+ * Used by other tools that need authenticated access
34
+ */
35
+ export declare function loadApiKey(): Promise<string | null>;