@telvok/librarian-mcp 1.5.4 → 2.3.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 (123) 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/sensitive-scanner.d.ts +20 -0
  5. package/dist/library/sensitive-scanner.js +56 -0
  6. package/dist/library/storage.d.ts +2 -2
  7. package/dist/library/storage.js +2 -2
  8. package/dist/library 2/embeddings.d.ts +21 -0
  9. package/dist/library 2/embeddings.js +86 -0
  10. package/dist/library 2/manager.d.ts +42 -0
  11. package/dist/library 2/manager.js +218 -0
  12. package/dist/library 2/parsers/cursor.d.ts +15 -0
  13. package/dist/library 2/parsers/cursor.js +168 -0
  14. package/dist/library 2/parsers/index.d.ts +6 -0
  15. package/dist/library 2/parsers/index.js +5 -0
  16. package/dist/library 2/parsers/json.d.ts +11 -0
  17. package/dist/library 2/parsers/json.js +95 -0
  18. package/dist/library 2/parsers/jsonl.d.ts +14 -0
  19. package/dist/library 2/parsers/jsonl.js +85 -0
  20. package/dist/library 2/parsers/markdown.d.ts +15 -0
  21. package/dist/library 2/parsers/markdown.js +77 -0
  22. package/dist/library 2/parsers/sqlite.d.ts +8 -0
  23. package/dist/library 2/parsers/sqlite.js +123 -0
  24. package/dist/library 2/parsers/types.d.ts +21 -0
  25. package/dist/library 2/parsers/types.js +4 -0
  26. package/dist/library 2/query.d.ts +26 -0
  27. package/dist/library 2/query.js +104 -0
  28. package/dist/library 2/schemas.d.ts +324 -0
  29. package/dist/library 2/schemas.js +79 -0
  30. package/dist/library 2/storage.d.ts +22 -0
  31. package/dist/library 2/storage.js +36 -0
  32. package/dist/library 2/vector-index.d.ts +55 -0
  33. package/dist/library 2/vector-index.js +160 -0
  34. package/dist/server 2.js +199 -0
  35. package/dist/server.d 2.ts +2 -0
  36. package/dist/server.js +104 -54
  37. package/dist/tools/adopt.d.ts +1 -0
  38. package/dist/tools/adopt.js +37 -10
  39. package/dist/tools/audit.d.ts +27 -0
  40. package/dist/tools/audit.js +126 -0
  41. package/dist/tools/auth.d.ts +69 -0
  42. package/dist/tools/auth.js +379 -0
  43. package/dist/tools/bounty-claim.d.ts +28 -0
  44. package/dist/tools/bounty-claim.js +92 -0
  45. package/dist/tools/bounty-create.d.ts +47 -0
  46. package/dist/tools/bounty-create.js +118 -0
  47. package/dist/tools/bounty-list.d.ts +50 -0
  48. package/dist/tools/bounty-list.js +116 -0
  49. package/dist/tools/bounty-submit.d.ts +34 -0
  50. package/dist/tools/bounty-submit.js +94 -0
  51. package/dist/tools/brief.d.ts +94 -0
  52. package/dist/tools/brief.js +234 -15
  53. package/dist/tools/delete.d.ts +87 -0
  54. package/dist/tools/delete.js +266 -0
  55. package/dist/tools/feedback.d.ts +27 -0
  56. package/dist/tools/feedback.js +98 -0
  57. package/dist/tools/help.d.ts +22 -0
  58. package/dist/tools/help.js +482 -0
  59. package/dist/tools/import-memories.d.ts +1 -0
  60. package/dist/tools/import-memories.js +18 -13
  61. package/dist/tools/index.d.ts +11 -0
  62. package/dist/tools/index.js +12 -0
  63. package/dist/tools/library-buy.d.ts +31 -0
  64. package/dist/tools/library-buy.js +104 -0
  65. package/dist/tools/library-download.d.ts +27 -0
  66. package/dist/tools/library-download.js +177 -0
  67. package/dist/tools/library-publish.d.ts +117 -0
  68. package/dist/tools/library-publish.js +447 -0
  69. package/dist/tools/library-search.d.ts +110 -0
  70. package/dist/tools/library-search.js +132 -0
  71. package/dist/tools/mark-hit.d.ts +1 -0
  72. package/dist/tools/mark-hit.js +83 -5
  73. package/dist/tools/my-books.d.ts +51 -0
  74. package/dist/tools/my-books.js +115 -0
  75. package/dist/tools/my-bounties.d.ts +43 -0
  76. package/dist/tools/my-bounties.js +126 -0
  77. package/dist/tools/rate-book.d.ts +40 -0
  78. package/dist/tools/rate-book.js +147 -0
  79. package/dist/tools/rebuild-index.d.ts +1 -0
  80. package/dist/tools/rebuild-index.js +40 -8
  81. package/dist/tools/record.d.ts +18 -0
  82. package/dist/tools/record.js +30 -26
  83. package/dist/tools/seller-analytics.d.ts +53 -0
  84. package/dist/tools/seller-analytics.js +180 -0
  85. package/dist/tools/sync.d.ts +55 -0
  86. package/dist/tools/sync.js +304 -0
  87. package/dist/tools/unsubscribe.d.ts +48 -0
  88. package/dist/tools/unsubscribe.js +120 -0
  89. package/dist/tools 2/adopt.d.ts +24 -0
  90. package/dist/tools 2/adopt.js +154 -0
  91. package/dist/tools 2/auth.d.ts +35 -0
  92. package/dist/tools 2/auth.js +229 -0
  93. package/dist/tools 2/brief.d.ts +56 -0
  94. package/dist/tools 2/brief.js +414 -0
  95. package/dist/tools 2/help.d.ts +21 -0
  96. package/dist/tools 2/help.js +267 -0
  97. package/dist/tools 2/import-memories.d.ts +32 -0
  98. package/dist/tools 2/import-memories.js +231 -0
  99. package/dist/tools 2/index.d.ts +12 -0
  100. package/dist/tools 2/index.js +12 -0
  101. package/dist/tools 2/mark-hit.d.ts +20 -0
  102. package/dist/tools 2/mark-hit.js +71 -0
  103. package/dist/tools 2/marketplace-buy.d.ts +30 -0
  104. package/dist/tools 2/marketplace-buy.js +97 -0
  105. package/dist/tools 2/marketplace-download.d.ts +26 -0
  106. package/dist/tools 2/marketplace-download.js +160 -0
  107. package/dist/tools 2/marketplace-publish.d.ts +111 -0
  108. package/dist/tools 2/marketplace-publish.js +377 -0
  109. package/dist/tools 2/marketplace-search.d.ts +57 -0
  110. package/dist/tools 2/marketplace-search.js +96 -0
  111. package/dist/tools 2/my-books.d.ts +50 -0
  112. package/dist/tools 2/my-books.js +107 -0
  113. package/dist/tools 2/rate-book.d.ts +39 -0
  114. package/dist/tools 2/rate-book.js +139 -0
  115. package/dist/tools 2/rebuild-index.d.ts +23 -0
  116. package/dist/tools 2/rebuild-index.js +107 -0
  117. package/dist/tools 2/record.d.ts +40 -0
  118. package/dist/tools 2/record.js +205 -0
  119. package/dist/tools 2/seller-analytics.d.ts +35 -0
  120. package/dist/tools 2/seller-analytics.js +102 -0
  121. package/dist/tools 2/sync.d.ts +54 -0
  122. package/dist/tools 2/sync.js +298 -0
  123. package/package.json +1 -1
@@ -0,0 +1,447 @@
1
+ // ============================================================================
2
+ // Marketplace Publish Tool
3
+ // Publish local entries as a book on Telvok library
4
+ // ============================================================================
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import * as crypto from 'crypto';
8
+ import { glob } from 'glob';
9
+ import matter from 'gray-matter';
10
+ import { loadApiKey } from './auth.js';
11
+ import { getLibraryPath, getLocalPath } from '../library/storage.js';
12
+ import { scanForSensitiveData } from '../library/sensitive-scanner.js';
13
+ const TELVOK_API_URL = process.env.TELVOK_API_URL || 'https://telvok.com';
14
+ const TOKEN_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes
15
+ let pendingPublish = null;
16
+ // ============================================================================
17
+ // Tool Definition
18
+ // ============================================================================
19
+ export const libraryPublishTool = {
20
+ name: 'library_publish',
21
+ title: 'Publish Book',
22
+ description: `Publish local entries as a book on Telvok library.
23
+
24
+ ⚠️ TWO-STEP PUBLISH FLOW (MANDATORY):
25
+
26
+ Step 1: ALWAYS call with preview: true first. This shows what will be published
27
+ and returns a publish_token. Show the preview to the user and ASK FOR CONFIRMATION.
28
+
29
+ Step 2: ONLY after the user explicitly confirms, call again with the publish_token
30
+ from the preview response. Publishing WITHOUT a valid token will be rejected.
31
+
32
+ DO NOT skip the preview. DO NOT publish without user confirmation.
33
+ The tool will refuse to publish without a valid publish_token from a preview.
34
+
35
+ TRIGGER PATTERNS:
36
+ - "Publish my entries" → library_publish({ name: "...", pricing: { type: "open" }, preview: true })
37
+ - User says "yes, publish it" → library_publish({ ..., publish_token: "<token from preview>" })
38
+
39
+ Examples:
40
+ - Preview: library_publish({ name: "My Book", pricing: { type: "open" }, preview: true })
41
+ - Publish: library_publish({ name: "My Book", pricing: { type: "open" }, consumption: "download", attestation: { original_work: true, no_secrets: true, terms_accepted: true }, publish_token: "abc123" })`,
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ name: {
46
+ type: 'string',
47
+ description: 'Book title (3-100 characters)',
48
+ },
49
+ description: {
50
+ type: 'string',
51
+ description: 'Short description of the book (optional, max 500 chars)',
52
+ },
53
+ pricing: {
54
+ type: 'object',
55
+ properties: {
56
+ type: {
57
+ type: 'string',
58
+ enum: ['open', 'one_time', 'subscription'],
59
+ description: 'Pricing model',
60
+ },
61
+ price_cents: {
62
+ type: 'number',
63
+ description: 'Price in cents (required for paid, min 100 = $1.00)',
64
+ },
65
+ },
66
+ required: ['type'],
67
+ description: 'Pricing configuration',
68
+ },
69
+ consumption: {
70
+ type: 'string',
71
+ enum: ['inline', 'reference', 'download'],
72
+ description: 'How buyers access content. download only for free books.',
73
+ },
74
+ attestation: {
75
+ type: 'object',
76
+ properties: {
77
+ original_work: { type: 'boolean', description: 'Confirm this is original work' },
78
+ no_secrets: { type: 'boolean', description: 'Confirm no secrets/credentials' },
79
+ terms_accepted: { type: 'boolean', description: 'Accept library terms' },
80
+ },
81
+ required: ['original_work', 'no_secrets', 'terms_accepted'],
82
+ description: 'Required confirmations before publishing',
83
+ },
84
+ preview: {
85
+ type: 'boolean',
86
+ description: 'If true, show what would be published without publishing. Returns a publish_token.',
87
+ },
88
+ publish_token: {
89
+ type: 'string',
90
+ description: 'Token from preview response. Required to actually publish. Single-use, expires in 5 minutes.',
91
+ },
92
+ entries: {
93
+ type: 'array',
94
+ items: { type: 'string' },
95
+ description: 'Specific entry filenames to include (omit for all local/)',
96
+ },
97
+ tags: {
98
+ type: 'array',
99
+ items: { type: 'string' },
100
+ description: 'Category/topic tags (max 10)',
101
+ },
102
+ license: {
103
+ type: 'string',
104
+ enum: ['open', 'open_attributed', 'personal'],
105
+ description: 'License type (default: personal)',
106
+ },
107
+ },
108
+ required: ['name', 'pricing'],
109
+ },
110
+ async handler(args) {
111
+ const { name, description, pricing, consumption, attestation, preview, publish_token, entries: entryFilter, tags, license } = args;
112
+ // Validate name
113
+ if (!name || typeof name !== 'string' || name.trim().length < 3) {
114
+ throw new Error('Book name is required (minimum 3 characters)');
115
+ }
116
+ if (name.trim().length > 100) {
117
+ throw new Error('Book name must be 100 characters or less');
118
+ }
119
+ // Validate pricing
120
+ if (!pricing || !pricing.type) {
121
+ throw new Error('Pricing type is required');
122
+ }
123
+ if (!['open', 'one_time', 'subscription'].includes(pricing.type)) {
124
+ throw new Error('Pricing type must be: open, one_time, or subscription');
125
+ }
126
+ if (pricing.type !== 'open' && (!pricing.price_cents || pricing.price_cents < 100)) {
127
+ throw new Error('Paid books require price_cents >= 100 ($1.00)');
128
+ }
129
+ if (pricing.price_cents && pricing.price_cents > 100000) {
130
+ throw new Error('Price cannot exceed $1000.00 (100000 cents)');
131
+ }
132
+ // Validate description length
133
+ if (description && description.length > 500) {
134
+ throw new Error('Description must be 500 characters or less');
135
+ }
136
+ // Validate tags count
137
+ if (tags && tags.length > 10) {
138
+ throw new Error('Maximum 10 tags allowed');
139
+ }
140
+ // Collect entries from local/ (needed for preview and publish)
141
+ const collectedEntries = await collectLocalEntries(entryFilter);
142
+ // Scan for sensitive data before publishing
143
+ const sensitiveFindings = scanForSensitiveData(collectedEntries);
144
+ if (sensitiveFindings.length > 0) {
145
+ const warnings = sensitiveFindings.map(f => ` ⚠ ${f.entry}: ${f.matches.join(', ')}`).join('\n');
146
+ if (preview) {
147
+ // In preview mode, show warnings but continue
148
+ return {
149
+ success: true,
150
+ preview: true,
151
+ message: `⚠ SENSITIVE DATA DETECTED in ${sensitiveFindings.length} entry(s):\n${warnings}\n\nReview these entries before publishing. Remove credentials, API keys, passwords, and personal data.`,
152
+ };
153
+ }
154
+ // In publish mode, block and require cleanup
155
+ return {
156
+ success: false,
157
+ message: `🚫 Publish blocked — sensitive data detected in ${sensitiveFindings.length} entry(s):\n${warnings}\n\nClean up these entries with record() or delete() before publishing. Use library_publish({ preview: true }) to re-check.`,
158
+ };
159
+ }
160
+ if (collectedEntries.length === 0) {
161
+ return {
162
+ success: false,
163
+ message: 'No entries found in .librarian/local/. Use record() to create entries first.',
164
+ };
165
+ }
166
+ // Format pricing display
167
+ const pricingDisplay = pricing.type === 'open'
168
+ ? 'Free'
169
+ : `$${((pricing.price_cents || 0) / 100).toFixed(2)}`;
170
+ // Handle preview mode - return summary with publish token
171
+ if (preview) {
172
+ const token = crypto.randomBytes(16).toString('hex');
173
+ pendingPublish = {
174
+ token,
175
+ name: name.trim(),
176
+ entries_count: collectedEntries.length,
177
+ created: Date.now(),
178
+ };
179
+ return {
180
+ success: true,
181
+ preview: true,
182
+ message: `Preview of "${name.trim()}" - NOT published yet.\n\n⚠️ Show this to the user and ask for confirmation before publishing.`,
183
+ publish_token: token,
184
+ summary: {
185
+ name: name.trim(),
186
+ pricing: { type: pricing.type, display: pricingDisplay },
187
+ entries_count: collectedEntries.length,
188
+ entries: collectedEntries.map(e => ({
189
+ title: e.title,
190
+ file: path.basename(e.originalPath),
191
+ })),
192
+ },
193
+ next_steps: 'Show preview to user. After they confirm, call library_publish() again with the publish_token to publish.',
194
+ };
195
+ }
196
+ // ========================================================================
197
+ // PUBLISH TOKEN VALIDATION
198
+ // Cannot publish without a valid token from preview
199
+ // ========================================================================
200
+ if (!publish_token) {
201
+ return {
202
+ success: false,
203
+ message: '🚫 Publishing requires a publish_token from a preview.\n\nYou must call library_publish({ preview: true, ... }) first, show the preview to the user, get their confirmation, then call again with the publish_token.\n\nThis is a safety measure to prevent accidental publishing.',
204
+ };
205
+ }
206
+ if (!pendingPublish || pendingPublish.token !== publish_token) {
207
+ return {
208
+ success: false,
209
+ message: '🚫 Invalid or expired publish_token. Run a new preview first with library_publish({ preview: true, ... }).',
210
+ };
211
+ }
212
+ if (Date.now() - pendingPublish.created > TOKEN_EXPIRY_MS) {
213
+ pendingPublish = null;
214
+ return {
215
+ success: false,
216
+ message: '🚫 Publish token expired (5 minute limit). Run a new preview first.',
217
+ };
218
+ }
219
+ // Token is valid — consume it (single use)
220
+ pendingPublish = null;
221
+ // Validate consumption type (required for actual publish)
222
+ if (!consumption) {
223
+ return {
224
+ success: false,
225
+ message: 'Consumption type required. Choose how buyers access your content:',
226
+ options: {
227
+ inline: 'Content returned in API responses (best for small entries)',
228
+ reference: 'README + pointers to entries (best for larger books)',
229
+ download: 'Download to local library (only for free/open books)',
230
+ },
231
+ };
232
+ }
233
+ if (!['inline', 'reference', 'download'].includes(consumption)) {
234
+ return {
235
+ success: false,
236
+ message: 'Invalid consumption type. Must be: inline, reference, or download',
237
+ };
238
+ }
239
+ if (consumption === 'download' && pricing.type !== 'open') {
240
+ return {
241
+ success: false,
242
+ message: 'Download is only for free books. Paid content uses inline or reference.',
243
+ next_steps: "Use pricing.type: 'open' for download, or consumption: 'inline'/'reference' for paid.",
244
+ };
245
+ }
246
+ // Validate attestation (required for actual publish)
247
+ if (!attestation) {
248
+ return {
249
+ success: false,
250
+ message: 'Attestation required. Please confirm:',
251
+ required: {
252
+ original_work: 'This is my original work or I have rights to publish',
253
+ no_secrets: 'Contains no secrets, credentials, or sensitive data',
254
+ terms_accepted: 'I accept the Telvok library terms',
255
+ },
256
+ };
257
+ }
258
+ const failedAttestations = [];
259
+ if (!attestation.original_work)
260
+ failedAttestations.push('original_work');
261
+ if (!attestation.no_secrets)
262
+ failedAttestations.push('no_secrets');
263
+ if (!attestation.terms_accepted)
264
+ failedAttestations.push('terms_accepted');
265
+ if (failedAttestations.length > 0) {
266
+ return {
267
+ success: false,
268
+ message: `All attestation fields must be true to publish. Failed: ${failedAttestations.join(', ')}`,
269
+ };
270
+ }
271
+ // Check authentication
272
+ const apiKey = await loadApiKey();
273
+ if (!apiKey) {
274
+ return {
275
+ success: false,
276
+ message: 'Not authenticated. Run auth({ action: "login" }) to connect your Telvok account first.',
277
+ };
278
+ }
279
+ // Format entries for API
280
+ const apiEntries = collectedEntries.map(e => ({
281
+ title: e.title,
282
+ content: e.content,
283
+ intent: e.intent,
284
+ context: e.context,
285
+ reasoning: e.reasoning,
286
+ example: e.example,
287
+ }));
288
+ // Build request body
289
+ const requestBody = {
290
+ name: name.trim(),
291
+ description: description?.trim(),
292
+ pricing,
293
+ consumption,
294
+ entries: apiEntries,
295
+ tags: tags || [],
296
+ license_type: license || 'personal',
297
+ attestation,
298
+ };
299
+ try {
300
+ const response = await fetch(`${TELVOK_API_URL}/api/publish`, {
301
+ method: 'POST',
302
+ headers: {
303
+ 'Authorization': `Bearer ${apiKey}`,
304
+ 'Content-Type': 'application/json',
305
+ },
306
+ body: JSON.stringify(requestBody),
307
+ });
308
+ const data = await response.json();
309
+ if (!response.ok) {
310
+ // Handle Stripe Connect requirement
311
+ if (data.error === 'stripe_connect_required') {
312
+ return {
313
+ success: false,
314
+ message: `Stripe Connect required to sell paid content.\n\nComplete setup at: ${data.setup_url}`,
315
+ setup_url: data.setup_url,
316
+ };
317
+ }
318
+ // Handle validation errors
319
+ if (data.error === 'validation_error') {
320
+ const details = Object.entries(data.details || {})
321
+ .map(([k, v]) => ` - ${k}: ${v}`)
322
+ .join('\n');
323
+ return {
324
+ success: false,
325
+ message: `Validation failed:\n${details}`,
326
+ };
327
+ }
328
+ return {
329
+ success: false,
330
+ message: data.error || `Publish failed: HTTP ${response.status}`,
331
+ };
332
+ }
333
+ return {
334
+ success: true,
335
+ message: data.message || `Published "${data.book?.name}" with ${data.entries_count} entries`,
336
+ book: data.book,
337
+ entries_count: data.entries_count,
338
+ };
339
+ }
340
+ catch (error) {
341
+ const message = error instanceof Error ? error.message : String(error);
342
+ throw new Error(`Publish failed: ${message}`);
343
+ }
344
+ },
345
+ };
346
+ // ============================================================================
347
+ // Entry Collection
348
+ // ============================================================================
349
+ async function collectLocalEntries(filter) {
350
+ const libraryPath = getLibraryPath();
351
+ const localPath = getLocalPath(libraryPath);
352
+ const entries = [];
353
+ try {
354
+ const files = await glob(path.join(localPath, '**/*.md'), { nodir: true });
355
+ for (const filePath of files) {
356
+ const filename = path.basename(filePath);
357
+ // If filter specified, only include matching files
358
+ if (filter && filter.length > 0) {
359
+ const matchesFilter = filter.some(f => filename === f ||
360
+ filename === f + '.md' ||
361
+ filePath.endsWith(f) ||
362
+ filePath.endsWith(f + '.md'));
363
+ if (!matchesFilter)
364
+ continue;
365
+ }
366
+ try {
367
+ const content = await fs.readFile(filePath, 'utf-8');
368
+ const parsed = parseEntryFile(content, filePath);
369
+ if (parsed) {
370
+ entries.push({
371
+ ...parsed,
372
+ originalPath: filePath,
373
+ });
374
+ }
375
+ }
376
+ catch {
377
+ // Skip files that can't be parsed
378
+ }
379
+ }
380
+ }
381
+ catch {
382
+ // No local directory yet
383
+ }
384
+ return entries;
385
+ }
386
+ function parseEntryFile(content, filePath) {
387
+ const { data: frontmatter, content: body } = matter(content);
388
+ const trimmedBody = body.trim();
389
+ if (!trimmedBody)
390
+ return null;
391
+ // Extract title from frontmatter, H1, or filename
392
+ let title = frontmatter.title;
393
+ if (!title) {
394
+ const headingMatch = trimmedBody.match(/^#\s+(.+)$/m);
395
+ if (headingMatch) {
396
+ title = headingMatch[1].trim();
397
+ }
398
+ else {
399
+ // Use filename as title, converting hyphens to spaces
400
+ title = path.basename(filePath, '.md')
401
+ .replace(/-/g, ' ')
402
+ .replace(/\b\w/g, l => l.toUpperCase());
403
+ }
404
+ }
405
+ // Extract sections from body
406
+ const sections = extractSections(trimmedBody);
407
+ return {
408
+ title,
409
+ content: sections.main || trimmedBody,
410
+ intent: frontmatter.intent || undefined,
411
+ context: frontmatter.context || undefined,
412
+ reasoning: sections.reasoning,
413
+ example: sections.example,
414
+ };
415
+ }
416
+ function extractSections(body) {
417
+ const result = {
418
+ main: body,
419
+ };
420
+ // Find ## Reasoning section
421
+ const reasoningMatch = body.match(/##\s*Reasoning\s*\n([\s\S]*?)(?=##|$)/i);
422
+ if (reasoningMatch) {
423
+ result.reasoning = reasoningMatch[1].trim();
424
+ }
425
+ // Find ## Example section
426
+ const exampleMatch = body.match(/##\s*Example\s*\n([\s\S]*?)(?=##|$)/i);
427
+ if (exampleMatch) {
428
+ result.example = exampleMatch[1].trim();
429
+ }
430
+ // Main content is everything after title until first ## section
431
+ const mainMatch = body.match(/^#\s+.+\n\n?([\s\S]*?)(?=##|$)/);
432
+ if (mainMatch) {
433
+ result.main = mainMatch[1].trim();
434
+ }
435
+ else {
436
+ // If no H1 header, take content before first ## section
437
+ const beforeSections = body.match(/^([\s\S]*?)(?=##)/);
438
+ if (beforeSections) {
439
+ result.main = beforeSections[1].trim();
440
+ }
441
+ }
442
+ return result;
443
+ }
444
+ // ============================================================================
445
+ // Export
446
+ // ============================================================================
447
+ export default libraryPublishTool;
@@ -0,0 +1,110 @@
1
+ interface BookResult {
2
+ slug: string;
3
+ name: string;
4
+ description: string;
5
+ pricing: 'open' | 'one_time' | 'subscription';
6
+ price: string;
7
+ entries: number;
8
+ rating: number | null;
9
+ tags: string[];
10
+ total_hits: number;
11
+ }
12
+ interface SearchResult {
13
+ books: BookResult[];
14
+ total: number;
15
+ message: string;
16
+ }
17
+ export declare const librarySearchTool: {
18
+ name: string;
19
+ title: string;
20
+ description: string;
21
+ inputSchema: {
22
+ type: "object";
23
+ properties: {
24
+ query: {
25
+ type: string;
26
+ description: string;
27
+ };
28
+ filters: {
29
+ type: string;
30
+ properties: {
31
+ pricing: {
32
+ type: string;
33
+ enum: string[];
34
+ description: string;
35
+ };
36
+ tags: {
37
+ type: string;
38
+ items: {
39
+ type: string;
40
+ };
41
+ description: string;
42
+ };
43
+ min_rating: {
44
+ type: string;
45
+ description: string;
46
+ };
47
+ };
48
+ description: string;
49
+ };
50
+ limit: {
51
+ type: string;
52
+ description: string;
53
+ };
54
+ };
55
+ required: never[];
56
+ };
57
+ outputSchema: {
58
+ type: "object";
59
+ properties: {
60
+ books: {
61
+ type: string;
62
+ items: {
63
+ type: string;
64
+ properties: {
65
+ slug: {
66
+ type: string;
67
+ };
68
+ name: {
69
+ type: string;
70
+ };
71
+ description: {
72
+ type: string;
73
+ };
74
+ pricing: {
75
+ type: string;
76
+ enum: string[];
77
+ };
78
+ price: {
79
+ type: string;
80
+ };
81
+ entries: {
82
+ type: string;
83
+ };
84
+ rating: {
85
+ type: string;
86
+ };
87
+ tags: {
88
+ type: string;
89
+ items: {
90
+ type: string;
91
+ };
92
+ };
93
+ total_hits: {
94
+ type: string;
95
+ };
96
+ };
97
+ };
98
+ };
99
+ total: {
100
+ type: string;
101
+ };
102
+ message: {
103
+ type: string;
104
+ };
105
+ };
106
+ required: string[];
107
+ };
108
+ handler(args: unknown): Promise<SearchResult>;
109
+ };
110
+ export {};
@@ -0,0 +1,132 @@
1
+ // ============================================================================
2
+ // Library Search Tool
3
+ // Search for books in the Telvok library
4
+ // ============================================================================
5
+ const TELVOK_API_URL = process.env.TELVOK_API_URL || 'https://telvok.com';
6
+ // ============================================================================
7
+ // Tool Definition
8
+ // ============================================================================
9
+ export const librarySearchTool = {
10
+ name: 'library_search',
11
+ title: 'Search Marketplace',
12
+ description: `Search Telvok marketplace for knowledge books.
13
+
14
+ USE THIS TOOL WHEN:
15
+ - brief() returns no results and marketplace might have relevant content
16
+ - User explicitly asks to find/search marketplace books
17
+ - Looking for domain expertise we don't have locally
18
+
19
+ DO NOT USE when brief() already found useful local entries.
20
+
21
+ TRIGGER PATTERNS:
22
+ - brief() found nothing → library_search({ query: "<same topic>" })
23
+ - "Find books about X" → library_search({ query: "X" })
24
+ - "What's in the marketplace?" → library_search({})
25
+ - "Show free books" → library_search({ filters: { pricing: "open" } })
26
+
27
+ Filters: pricing ("open"/"one_time"/"subscription"), tags, min_rating (0-5)
28
+
29
+ Examples:
30
+ - library_search({ query: "react hooks" })
31
+ - library_search({ filters: { pricing: "open" } })`,
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ query: {
36
+ type: 'string',
37
+ description: 'Search terms to find books',
38
+ },
39
+ filters: {
40
+ type: 'object',
41
+ properties: {
42
+ pricing: {
43
+ type: 'string',
44
+ enum: ['open', 'one_time', 'subscription'],
45
+ description: 'Filter by pricing type',
46
+ },
47
+ tags: {
48
+ type: 'array',
49
+ items: { type: 'string' },
50
+ description: 'Filter by tags',
51
+ },
52
+ min_rating: {
53
+ type: 'number',
54
+ description: 'Minimum quality rating (0-5)',
55
+ },
56
+ },
57
+ description: 'Optional filters to narrow results',
58
+ },
59
+ limit: {
60
+ type: 'number',
61
+ description: 'Maximum results to return (default: 10)',
62
+ },
63
+ },
64
+ required: [], // Query is optional - empty returns all books (browse mode)
65
+ },
66
+ outputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ books: {
70
+ type: 'array',
71
+ items: {
72
+ type: 'object',
73
+ properties: {
74
+ slug: { type: 'string' },
75
+ name: { type: 'string' },
76
+ description: { type: 'string' },
77
+ pricing: { type: 'string', enum: ['open', 'one_time', 'subscription'] },
78
+ price: { type: 'string' },
79
+ entries: { type: 'number' },
80
+ rating: { type: 'number' },
81
+ tags: { type: 'array', items: { type: 'string' } },
82
+ total_hits: { type: 'number' },
83
+ },
84
+ },
85
+ },
86
+ total: { type: 'number' },
87
+ message: { type: 'string' },
88
+ },
89
+ required: ['books', 'total', 'message'],
90
+ },
91
+ async handler(args) {
92
+ const { query = '', filters, limit = 10 } = args;
93
+ try {
94
+ const response = await fetch(`${TELVOK_API_URL}/api/search`, {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ query, filters, limit }),
98
+ });
99
+ if (!response.ok) {
100
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
101
+ throw new Error(error.error || `Search failed: HTTP ${response.status}`);
102
+ }
103
+ const data = await response.json();
104
+ // Format results for agent consumption
105
+ const books = data.books || [];
106
+ const total = data.total || 0;
107
+ if (books.length === 0) {
108
+ const queryMsg = query ? `for "${query}"` : 'matching your filters';
109
+ return {
110
+ books: [],
111
+ total: 0,
112
+ message: `No books found ${queryMsg}. Try different search terms or filters.`,
113
+ };
114
+ }
115
+ // Build helpful summary
116
+ const summary = books.map((b, i) => {
117
+ const helpedText = b.total_hits > 0 ? `Helped ${b.total_hits} agent${b.total_hits === 1 ? '' : 's'}` : `${b.entries} entries`;
118
+ return `${i + 1}. **${b.name}** (${b.price}) - ${helpedText}\n ${b.description?.slice(0, 100)}${b.description?.length > 100 ? '...' : ''}`;
119
+ }).join('\n');
120
+ const queryMsg = query ? `for "${query}"` : 'in library';
121
+ return {
122
+ books,
123
+ total,
124
+ message: `Found ${total} book(s) ${queryMsg}:\n\n${summary}\n\nUse library_buy({ slug: "..." }) to purchase a book.`,
125
+ };
126
+ }
127
+ catch (error) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ throw new Error(`Library search failed: ${message}`);
130
+ }
131
+ },
132
+ };