@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.
- package/dist/library/errors.d.ts +48 -0
- package/dist/library/errors.js +80 -0
- package/dist/library/schemas.d.ts +6 -6
- package/dist/library/storage.d.ts +2 -2
- package/dist/library/storage.js +2 -2
- package/dist/library 2/embeddings.d.ts +21 -0
- package/dist/library 2/embeddings.js +86 -0
- package/dist/library 2/manager.d.ts +42 -0
- package/dist/library 2/manager.js +218 -0
- package/dist/library 2/parsers/cursor.d.ts +15 -0
- package/dist/library 2/parsers/cursor.js +168 -0
- package/dist/library 2/parsers/index.d.ts +6 -0
- package/dist/library 2/parsers/index.js +5 -0
- package/dist/library 2/parsers/json.d.ts +11 -0
- package/dist/library 2/parsers/json.js +95 -0
- package/dist/library 2/parsers/jsonl.d.ts +14 -0
- package/dist/library 2/parsers/jsonl.js +85 -0
- package/dist/library 2/parsers/markdown.d.ts +15 -0
- package/dist/library 2/parsers/markdown.js +77 -0
- package/dist/library 2/parsers/sqlite.d.ts +8 -0
- package/dist/library 2/parsers/sqlite.js +123 -0
- package/dist/library 2/parsers/types.d.ts +21 -0
- package/dist/library 2/parsers/types.js +4 -0
- package/dist/library 2/query.d.ts +26 -0
- package/dist/library 2/query.js +104 -0
- package/dist/library 2/schemas.d.ts +324 -0
- package/dist/library 2/schemas.js +79 -0
- package/dist/library 2/storage.d.ts +22 -0
- package/dist/library 2/storage.js +36 -0
- package/dist/library 2/vector-index.d.ts +55 -0
- package/dist/library 2/vector-index.js +160 -0
- package/dist/server 2.js +199 -0
- package/dist/server.d 2.ts +2 -0
- package/dist/server.js +102 -54
- package/dist/tools/adopt.d.ts +1 -0
- package/dist/tools/adopt.js +37 -10
- package/dist/tools/auth.d.ts +69 -0
- package/dist/tools/auth.js +379 -0
- package/dist/tools/bounty-claim.d.ts +28 -0
- package/dist/tools/bounty-claim.js +92 -0
- package/dist/tools/bounty-create.d.ts +47 -0
- package/dist/tools/bounty-create.js +118 -0
- package/dist/tools/bounty-list.d.ts +50 -0
- package/dist/tools/bounty-list.js +116 -0
- package/dist/tools/bounty-submit.d.ts +34 -0
- package/dist/tools/bounty-submit.js +94 -0
- package/dist/tools/brief.d.ts +94 -0
- package/dist/tools/brief.js +234 -15
- package/dist/tools/delete.d.ts +87 -0
- package/dist/tools/delete.js +266 -0
- package/dist/tools/feedback.d.ts +27 -0
- package/dist/tools/feedback.js +98 -0
- package/dist/tools/help.d.ts +22 -0
- package/dist/tools/help.js +482 -0
- package/dist/tools/import-memories.d.ts +1 -0
- package/dist/tools/import-memories.js +18 -13
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/library-buy.d.ts +31 -0
- package/dist/tools/library-buy.js +104 -0
- package/dist/tools/library-download.d.ts +27 -0
- package/dist/tools/library-download.js +177 -0
- package/dist/tools/library-publish.d.ts +112 -0
- package/dist/tools/library-publish.js +387 -0
- package/dist/tools/library-search.d.ts +110 -0
- package/dist/tools/library-search.js +132 -0
- package/dist/tools/mark-hit.d.ts +1 -0
- package/dist/tools/mark-hit.js +83 -5
- package/dist/tools/my-books.d.ts +51 -0
- package/dist/tools/my-books.js +115 -0
- package/dist/tools/my-bounties.d.ts +43 -0
- package/dist/tools/my-bounties.js +126 -0
- package/dist/tools/rate-book.d.ts +40 -0
- package/dist/tools/rate-book.js +147 -0
- package/dist/tools/rebuild-index.d.ts +1 -0
- package/dist/tools/rebuild-index.js +40 -8
- package/dist/tools/record.d.ts +18 -0
- package/dist/tools/record.js +30 -26
- package/dist/tools/seller-analytics.d.ts +53 -0
- package/dist/tools/seller-analytics.js +180 -0
- package/dist/tools/sync.d.ts +55 -0
- package/dist/tools/sync.js +304 -0
- package/dist/tools/unsubscribe.d.ts +48 -0
- package/dist/tools/unsubscribe.js +120 -0
- package/dist/tools 2/adopt.d.ts +24 -0
- package/dist/tools 2/adopt.js +154 -0
- package/dist/tools 2/auth.d.ts +35 -0
- package/dist/tools 2/auth.js +229 -0
- package/dist/tools 2/brief.d.ts +56 -0
- package/dist/tools 2/brief.js +414 -0
- package/dist/tools 2/help.d.ts +21 -0
- package/dist/tools 2/help.js +267 -0
- package/dist/tools 2/import-memories.d.ts +32 -0
- package/dist/tools 2/import-memories.js +231 -0
- package/dist/tools 2/index.d.ts +12 -0
- package/dist/tools 2/index.js +12 -0
- package/dist/tools 2/mark-hit.d.ts +20 -0
- package/dist/tools 2/mark-hit.js +71 -0
- package/dist/tools 2/marketplace-buy.d.ts +30 -0
- package/dist/tools 2/marketplace-buy.js +97 -0
- package/dist/tools 2/marketplace-download.d.ts +26 -0
- package/dist/tools 2/marketplace-download.js +160 -0
- package/dist/tools 2/marketplace-publish.d.ts +111 -0
- package/dist/tools 2/marketplace-publish.js +377 -0
- package/dist/tools 2/marketplace-search.d.ts +57 -0
- package/dist/tools 2/marketplace-search.js +96 -0
- package/dist/tools 2/my-books.d.ts +50 -0
- package/dist/tools 2/my-books.js +107 -0
- package/dist/tools 2/rate-book.d.ts +39 -0
- package/dist/tools 2/rate-book.js +139 -0
- package/dist/tools 2/rebuild-index.d.ts +23 -0
- package/dist/tools 2/rebuild-index.js +107 -0
- package/dist/tools 2/record.d.ts +40 -0
- package/dist/tools 2/record.js +205 -0
- package/dist/tools 2/seller-analytics.d.ts +35 -0
- package/dist/tools 2/seller-analytics.js +102 -0
- package/dist/tools 2/sync.d.ts +54 -0
- package/dist/tools 2/sync.js +298 -0
- 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>;
|