@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,229 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { getLibraryPath } from '../library/storage.js';
4
+ // ============================================================================
5
+ // Constants
6
+ // ============================================================================
7
+ const TELVOK_API_URL = process.env.TELVOK_API_URL || 'https://telvok.com';
8
+ const POLL_INTERVAL_MS = 5000; // 5 seconds
9
+ const MAX_POLL_ATTEMPTS = 120; // 10 minutes / 5 seconds
10
+ // ============================================================================
11
+ // Tool Definition
12
+ // ============================================================================
13
+ export const authTool = {
14
+ name: 'auth',
15
+ description: `Handle Telvok marketplace authentication.
16
+
17
+ Use this to connect your agent to your Telvok account.
18
+
19
+ Actions:
20
+ - login: Start device code flow. Returns a code to enter at telvok.com/device
21
+ - complete: After user authorizes, call this to finish login and save credentials
22
+ - logout: Remove stored credentials
23
+ - status: Check if authenticated and show current user
24
+
25
+ Examples:
26
+ - auth({ action: 'login' }) → Get code, visit URL, authorize
27
+ - auth({ action: 'complete' }) → After authorizing, complete the login
28
+ - auth({ action: 'status' }) → Check if logged in
29
+ - auth({ action: 'logout' }) → Clear credentials`,
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ action: {
34
+ type: 'string',
35
+ enum: ['login', 'complete', 'logout', 'status'],
36
+ description: 'Auth action to perform',
37
+ },
38
+ },
39
+ required: ['action'],
40
+ },
41
+ async handler(args) {
42
+ const { action } = args;
43
+ const libraryPath = getLibraryPath();
44
+ const authFile = path.join(libraryPath, '.auth');
45
+ const pendingFile = path.join(libraryPath, '.auth-pending');
46
+ switch (action) {
47
+ case 'status':
48
+ return await checkStatus(authFile);
49
+ case 'logout':
50
+ return await logout(authFile);
51
+ case 'login':
52
+ return await login(authFile, pendingFile);
53
+ case 'complete':
54
+ return await completeLogin(authFile, pendingFile);
55
+ default:
56
+ throw new Error(`Unknown action: ${action}`);
57
+ }
58
+ },
59
+ };
60
+ // ============================================================================
61
+ // Action Handlers
62
+ // ============================================================================
63
+ async function checkStatus(authFile) {
64
+ try {
65
+ const content = await fs.readFile(authFile, 'utf-8');
66
+ const data = JSON.parse(content);
67
+ return {
68
+ authenticated: true,
69
+ user_email: data.user_email,
70
+ user_id: data.user_id,
71
+ message: `Authenticated as ${data.user_email}`,
72
+ };
73
+ }
74
+ catch {
75
+ return {
76
+ authenticated: false,
77
+ message: 'Not authenticated. Use auth({ action: "login" }) to connect your Telvok account.',
78
+ };
79
+ }
80
+ }
81
+ async function logout(authFile) {
82
+ try {
83
+ await fs.unlink(authFile);
84
+ return {
85
+ authenticated: false,
86
+ message: 'Logged out successfully. Credentials removed.',
87
+ };
88
+ }
89
+ catch {
90
+ return {
91
+ authenticated: false,
92
+ message: 'Already logged out.',
93
+ };
94
+ }
95
+ }
96
+ async function login(authFile, pendingFile) {
97
+ // Check if already authenticated
98
+ try {
99
+ const content = await fs.readFile(authFile, 'utf-8');
100
+ const data = JSON.parse(content);
101
+ return {
102
+ authenticated: true,
103
+ user_email: data.user_email,
104
+ user_id: data.user_id,
105
+ message: `Already authenticated as ${data.user_email}. Use auth({ action: "logout" }) first to switch accounts.`,
106
+ };
107
+ }
108
+ catch {
109
+ // Not authenticated, proceed with login
110
+ }
111
+ // Request a new device code
112
+ try {
113
+ const response = await fetch(`${TELVOK_API_URL}/api/auth/device`, {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ });
117
+ if (!response.ok) {
118
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
119
+ throw new Error(error.error || `HTTP ${response.status}`);
120
+ }
121
+ const data = await response.json();
122
+ // Save device code to pending file for later completion
123
+ const libraryPath = getLibraryPath();
124
+ await fs.mkdir(libraryPath, { recursive: true });
125
+ await fs.writeFile(pendingFile, JSON.stringify({
126
+ device_code: data.device_code,
127
+ user_code: data.user_code,
128
+ verification_url: data.verification_url,
129
+ created_at: new Date().toISOString(),
130
+ }, null, 2), 'utf-8');
131
+ // Return immediately with direct auth URL
132
+ const directAuthUrl = `${TELVOK_API_URL}/auth/${data.device_code}`;
133
+ return {
134
+ authenticated: false,
135
+ verification_url: directAuthUrl,
136
+ user_code: data.user_code,
137
+ message: `Click to authorize: ${directAuthUrl}\n\nAfter authorizing, call auth({ action: "complete" }) to finish.`,
138
+ };
139
+ }
140
+ catch (error) {
141
+ const message = error instanceof Error ? error.message : String(error);
142
+ throw new Error(`Failed to start login: ${message}`);
143
+ }
144
+ }
145
+ async function completeLogin(authFile, pendingFile) {
146
+ // Read pending device code
147
+ let deviceCode;
148
+ try {
149
+ const content = await fs.readFile(pendingFile, 'utf-8');
150
+ const pending = JSON.parse(content);
151
+ deviceCode = pending.device_code;
152
+ }
153
+ catch {
154
+ return {
155
+ authenticated: false,
156
+ message: 'No pending login. Call auth({ action: "login" }) first.',
157
+ };
158
+ }
159
+ // Poll for completion (try a few times)
160
+ for (let attempt = 0; attempt < 3; attempt++) {
161
+ try {
162
+ const response = await fetch(`${TELVOK_API_URL}/api/auth/device/poll`, {
163
+ method: 'POST',
164
+ headers: { 'Content-Type': 'application/json' },
165
+ body: JSON.stringify({ device_code: deviceCode }),
166
+ });
167
+ const data = await response.json();
168
+ if (data.status === 'pending') {
169
+ await sleep(2000);
170
+ continue;
171
+ }
172
+ if (data.status === 'expired') {
173
+ await fs.unlink(pendingFile).catch(() => { });
174
+ throw new Error('Code expired. Please call auth({ action: "login" }) to get a new code.');
175
+ }
176
+ if (data.status === 'success') {
177
+ // Save credentials
178
+ const authData = {
179
+ api_key: data.api_key,
180
+ user_email: data.user.email,
181
+ user_id: data.user.id,
182
+ created_at: new Date().toISOString(),
183
+ };
184
+ await fs.writeFile(authFile, JSON.stringify(authData, null, 2), 'utf-8');
185
+ await fs.unlink(pendingFile).catch(() => { });
186
+ return {
187
+ authenticated: true,
188
+ user_email: data.user.email,
189
+ user_id: data.user.id,
190
+ message: `Successfully authenticated as ${data.user.email}`,
191
+ };
192
+ }
193
+ throw new Error(`Unexpected status: ${data.status}`);
194
+ }
195
+ catch (error) {
196
+ if (error instanceof Error && (error.message.includes('expired') || error.message.includes('Unexpected'))) {
197
+ throw error;
198
+ }
199
+ // Network error, try again
200
+ await sleep(1000);
201
+ }
202
+ }
203
+ return {
204
+ authenticated: false,
205
+ message: 'Authorization not yet complete. Make sure you authorized at telvok.com/device, then call auth({ action: "complete" }) again.',
206
+ };
207
+ }
208
+ // ============================================================================
209
+ // Helpers
210
+ // ============================================================================
211
+ function sleep(ms) {
212
+ return new Promise(resolve => setTimeout(resolve, ms));
213
+ }
214
+ /**
215
+ * Load saved API key from auth file
216
+ * Used by other tools that need authenticated access
217
+ */
218
+ export async function loadApiKey() {
219
+ try {
220
+ const libraryPath = getLibraryPath();
221
+ const authFile = path.join(libraryPath, '.auth');
222
+ const content = await fs.readFile(authFile, 'utf-8');
223
+ const data = JSON.parse(content);
224
+ return data.api_key;
225
+ }
226
+ catch {
227
+ return null;
228
+ }
229
+ }
@@ -0,0 +1,56 @@
1
+ export interface BriefEntry {
2
+ title: string;
3
+ intent: string | null;
4
+ context: string | null;
5
+ preview: string;
6
+ path: string;
7
+ created: string;
8
+ hits: number;
9
+ last_hit: string | null;
10
+ source?: 'local' | 'cloud' | 'packages';
11
+ book_name?: string;
12
+ book_slug?: string;
13
+ }
14
+ export interface MarketplaceBook {
15
+ slug: string;
16
+ name: string;
17
+ description: string;
18
+ pricing: string;
19
+ price: string;
20
+ entries: number;
21
+ }
22
+ export interface BriefResult {
23
+ entries: BriefEntry[];
24
+ total: number;
25
+ message: string;
26
+ libraryPath: string;
27
+ marketplace?: {
28
+ books: MarketplaceBook[];
29
+ total: number;
30
+ };
31
+ }
32
+ export declare const briefTool: {
33
+ name: string;
34
+ description: string;
35
+ inputSchema: {
36
+ type: "object";
37
+ properties: {
38
+ query: {
39
+ type: string;
40
+ description: string;
41
+ };
42
+ limit: {
43
+ type: string;
44
+ description: string;
45
+ default: number;
46
+ };
47
+ include_marketplace: {
48
+ type: string;
49
+ description: string;
50
+ default: boolean;
51
+ };
52
+ };
53
+ required: never[];
54
+ };
55
+ handler(args: unknown): Promise<BriefResult>;
56
+ };
@@ -0,0 +1,414 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import matter from 'gray-matter';
4
+ import { glob } from 'glob';
5
+ import { getLibraryPath, getLocalPath, getImportedPath, getPackagesPath } from '../library/storage.js';
6
+ import { loadIndex, semanticSearch, isIndexStale } from '../library/vector-index.js';
7
+ import { loadApiKey } from './auth.js';
8
+ // ============================================================================
9
+ // Tool Definition
10
+ // ============================================================================
11
+ const TELVOK_API_URL = process.env.TELVOK_API_URL || 'https://telvok.com';
12
+ export const briefTool = {
13
+ name: 'brief',
14
+ description: `Check what we already know before diving in.
15
+
16
+ We've solved problems before. Before thinking through a problem, making
17
+ decisions, or planning - brief yourself on what past-us figured out.
18
+ Searches intent, insight, context, and examples.
19
+
20
+ Examples:
21
+ - brief({ query: "stripe webhooks" })
22
+ - brief({ query: "auth token" })
23
+ - brief({}) → returns recent entries
24
+ - brief({ query: "react", include_marketplace: true }) → also search marketplace`,
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ query: {
29
+ type: 'string',
30
+ description: 'What are we working on? Searches our library. Leave empty to see recent entries.',
31
+ },
32
+ limit: {
33
+ type: 'number',
34
+ description: 'Max entries to return',
35
+ default: 5,
36
+ },
37
+ include_marketplace: {
38
+ type: 'boolean',
39
+ description: 'Also search Telvok marketplace for relevant books',
40
+ default: false,
41
+ },
42
+ },
43
+ required: [],
44
+ },
45
+ async handler(args) {
46
+ const { query, limit = 5, include_marketplace = false } = args;
47
+ const libraryPath = getLibraryPath();
48
+ const localPath = getLocalPath(libraryPath);
49
+ const importedPath = getImportedPath(libraryPath);
50
+ const packagesPath = getPackagesPath(libraryPath);
51
+ let allEntries = [];
52
+ let useSemanticSearch = false;
53
+ let semanticMatches = [];
54
+ // Try semantic search if query is provided
55
+ if (query) {
56
+ try {
57
+ const index = await loadIndex();
58
+ // Only use semantic search if index has entries and isn't stale
59
+ if (index.entries.length > 0 && !isIndexStale(index)) {
60
+ semanticMatches = await semanticSearch(index, query, limit);
61
+ useSemanticSearch = semanticMatches.length > 0;
62
+ }
63
+ }
64
+ catch {
65
+ // Semantic search unavailable, fall back to keyword search
66
+ useSemanticSearch = false;
67
+ }
68
+ }
69
+ if (useSemanticSearch && semanticMatches.length > 0) {
70
+ // Load only the entries that matched semantically
71
+ const matchedPaths = new Set(semanticMatches.map(m => m.path));
72
+ for (const match of semanticMatches) {
73
+ const fullPath = path.join(libraryPath, match.path);
74
+ const entry = await readEntry(fullPath, libraryPath);
75
+ if (entry) {
76
+ allEntries.push(entry);
77
+ }
78
+ }
79
+ // Sort by semantic similarity (order preserved from semanticSearch)
80
+ // Re-order allEntries to match semanticMatches order
81
+ const pathToEntry = new Map(allEntries.map(e => [e.path, e]));
82
+ allEntries = semanticMatches
83
+ .map(m => pathToEntry.get(m.path))
84
+ .filter((e) => e !== undefined);
85
+ const total = allEntries.length;
86
+ const entries = allEntries.slice(0, limit);
87
+ // Optionally fetch cloud content and marketplace results
88
+ let cloudResult;
89
+ let marketplaceResult;
90
+ if (include_marketplace && query) {
91
+ // Fetch cloud content from owned books (in parallel with marketplace)
92
+ const [cloudData, marketplaceData] = await Promise.all([
93
+ fetchCloudContent(query, limit),
94
+ fetchMarketplaceResults(query, 5),
95
+ ]);
96
+ cloudResult = cloudData;
97
+ marketplaceResult = marketplaceData;
98
+ // Filter marketplace to exclude books user already owns
99
+ // (we got cloud results from them, so they own them)
100
+ if (cloudResult.entries.length > 0) {
101
+ const ownedSlugs = new Set(cloudResult.entries.map(e => e.book_slug));
102
+ marketplaceResult.books = marketplaceResult.books.filter(b => !ownedSlugs.has(b.slug));
103
+ marketplaceResult.total = marketplaceResult.books.length;
104
+ }
105
+ }
106
+ // Convert cloud entries to BriefEntry format and merge
107
+ let finalEntries = [...entries];
108
+ if (cloudResult && cloudResult.entries.length > 0) {
109
+ const cloudBriefEntries = cloudResult.entries.map(ce => ({
110
+ title: ce.title,
111
+ intent: ce.intent,
112
+ context: ce.context,
113
+ preview: ce.insight.length > 100 ? ce.insight.slice(0, 100) + '...' : ce.insight,
114
+ path: `cloud:${ce.book_slug}`, // Virtual path for cloud entries
115
+ created: new Date().toISOString(),
116
+ hits: 0,
117
+ last_hit: null,
118
+ source: 'cloud',
119
+ book_name: ce.book_name,
120
+ book_slug: ce.book_slug,
121
+ }));
122
+ // Interleave cloud entries with local: put cloud first (paid content priority)
123
+ // then fill remaining slots with local entries
124
+ const cloudCount = Math.min(cloudBriefEntries.length, Math.ceil(limit / 2));
125
+ const localCount = limit - cloudCount;
126
+ finalEntries = [
127
+ ...cloudBriefEntries.slice(0, cloudCount),
128
+ ...entries.slice(0, localCount),
129
+ ];
130
+ }
131
+ let message = `Found ${total} local ${total === 1 ? 'entry' : 'entries'} for "${query}" (semantic search).`;
132
+ if (cloudResult && cloudResult.entries.length > 0) {
133
+ message += ` Also found ${cloudResult.total} matching entries from owned books.`;
134
+ }
135
+ if (marketplaceResult && marketplaceResult.books.length > 0) {
136
+ message += ` ${marketplaceResult.total} book(s) available on marketplace.`;
137
+ }
138
+ return {
139
+ entries: finalEntries,
140
+ total: finalEntries.length,
141
+ message,
142
+ libraryPath: localPath,
143
+ marketplace: marketplaceResult,
144
+ };
145
+ }
146
+ // Fall back to keyword search
147
+ // Read local entries
148
+ try {
149
+ const localFiles = await glob(path.join(localPath, '**/*.md'), { nodir: true });
150
+ for (const filePath of localFiles) {
151
+ const entry = await readEntry(filePath, libraryPath);
152
+ if (entry) {
153
+ allEntries.push(entry);
154
+ }
155
+ }
156
+ }
157
+ catch {
158
+ // No local files yet
159
+ }
160
+ // Read imported entries (legacy - deprecated)
161
+ try {
162
+ const importedFiles = await glob(path.join(importedPath, '**/*.md'), { nodir: true });
163
+ for (const filePath of importedFiles) {
164
+ const entry = await readEntry(filePath, libraryPath);
165
+ if (entry) {
166
+ allEntries.push(entry);
167
+ }
168
+ }
169
+ }
170
+ catch {
171
+ // No imported files
172
+ }
173
+ // Read packages entries (marketplace content)
174
+ try {
175
+ const packagesFiles = await glob(path.join(packagesPath, '**/*.md'), { nodir: true });
176
+ for (const filePath of packagesFiles) {
177
+ const entry = await readEntry(filePath, libraryPath);
178
+ if (entry) {
179
+ allEntries.push(entry);
180
+ }
181
+ }
182
+ }
183
+ catch {
184
+ // No packages files
185
+ }
186
+ // If no entries at all
187
+ if (allEntries.length === 0) {
188
+ return {
189
+ entries: [],
190
+ total: 0,
191
+ message: 'No entries yet. Start recording!',
192
+ libraryPath: localPath,
193
+ };
194
+ }
195
+ // Filter by query if provided
196
+ if (query) {
197
+ const searchTerm = query.toLowerCase();
198
+ allEntries = allEntries.filter(entry => matchesSearch(entry, searchTerm));
199
+ }
200
+ // Sort by blended score: 60% recency + 40% hits
201
+ // Entries that helped before bubble up, but new entries still surface
202
+ allEntries = rankEntries(allEntries);
203
+ const total = allEntries.length;
204
+ // Apply limit
205
+ const entries = allEntries.slice(0, limit);
206
+ // Optionally fetch cloud content and marketplace results
207
+ let cloudResult;
208
+ let marketplaceResult;
209
+ if (include_marketplace && query) {
210
+ // Fetch cloud content from owned books (in parallel with marketplace)
211
+ const [cloudData, marketplaceData] = await Promise.all([
212
+ fetchCloudContent(query, limit),
213
+ fetchMarketplaceResults(query, 5),
214
+ ]);
215
+ cloudResult = cloudData;
216
+ marketplaceResult = marketplaceData;
217
+ // Filter marketplace to exclude books user already owns
218
+ if (cloudResult.entries.length > 0) {
219
+ const ownedSlugs = new Set(cloudResult.entries.map(e => e.book_slug));
220
+ marketplaceResult.books = marketplaceResult.books.filter(b => !ownedSlugs.has(b.slug));
221
+ marketplaceResult.total = marketplaceResult.books.length;
222
+ }
223
+ }
224
+ // Convert cloud entries to BriefEntry format and merge
225
+ let finalEntries = [...entries];
226
+ if (cloudResult && cloudResult.entries.length > 0) {
227
+ const cloudBriefEntries = cloudResult.entries.map(ce => ({
228
+ title: ce.title,
229
+ intent: ce.intent,
230
+ context: ce.context,
231
+ preview: ce.insight.length > 100 ? ce.insight.slice(0, 100) + '...' : ce.insight,
232
+ path: `cloud:${ce.book_slug}`, // Virtual path for cloud entries
233
+ created: new Date().toISOString(),
234
+ hits: 0,
235
+ last_hit: null,
236
+ source: 'cloud',
237
+ book_name: ce.book_name,
238
+ book_slug: ce.book_slug,
239
+ }));
240
+ finalEntries = [...entries, ...cloudBriefEntries].slice(0, limit);
241
+ }
242
+ // Build message
243
+ let message;
244
+ if (query) {
245
+ message = total === 0
246
+ ? `No local entries found for "${query}".`
247
+ : `Found ${total} local ${total === 1 ? 'entry' : 'entries'} for "${query}".`;
248
+ }
249
+ else {
250
+ message = `${total} ${total === 1 ? 'entry' : 'entries'} in library.`;
251
+ }
252
+ if (cloudResult && cloudResult.entries.length > 0) {
253
+ message += ` Also found ${cloudResult.total} matching entries from owned books.`;
254
+ }
255
+ if (marketplaceResult && marketplaceResult.books.length > 0) {
256
+ message += ` ${marketplaceResult.total} book(s) available on marketplace.`;
257
+ }
258
+ return {
259
+ entries: finalEntries,
260
+ total: finalEntries.length,
261
+ message,
262
+ libraryPath: localPath,
263
+ marketplace: marketplaceResult,
264
+ };
265
+ },
266
+ };
267
+ // ============================================================================
268
+ // Helper Functions
269
+ // ============================================================================
270
+ async function readEntry(filePath, libraryPath) {
271
+ try {
272
+ const content = await fs.readFile(filePath, 'utf-8');
273
+ const { data, content: body } = matter(content);
274
+ // Extract title from H1 or filename
275
+ let title = data.title;
276
+ if (!title) {
277
+ const headingMatch = body.match(/^#\s+(.+)$/m);
278
+ if (headingMatch) {
279
+ title = headingMatch[1].trim();
280
+ }
281
+ else {
282
+ title = path.basename(filePath, '.md').replace(/-/g, ' ');
283
+ }
284
+ }
285
+ // Extract preview - first 100 chars of body content
286
+ const bodyText = body.trim();
287
+ const preview = bodyText.length > 100
288
+ ? bodyText.slice(0, 100) + '...'
289
+ : bodyText;
290
+ return {
291
+ title,
292
+ intent: data.intent || null,
293
+ context: data.context || null,
294
+ preview,
295
+ path: path.relative(libraryPath, filePath),
296
+ created: data.created || new Date().toISOString(),
297
+ hits: typeof data.hits === 'number' ? data.hits : 0,
298
+ last_hit: data.last_hit || null,
299
+ };
300
+ }
301
+ catch {
302
+ return null;
303
+ }
304
+ }
305
+ function matchesSearch(entry, searchTerm) {
306
+ // Check title
307
+ if (entry.title.toLowerCase().includes(searchTerm)) {
308
+ return true;
309
+ }
310
+ // Check intent
311
+ if (entry.intent && entry.intent.toLowerCase().includes(searchTerm)) {
312
+ return true;
313
+ }
314
+ // Check context
315
+ if (entry.context && entry.context.toLowerCase().includes(searchTerm)) {
316
+ return true;
317
+ }
318
+ // Check preview (basic substring match - Claude does semantic filtering)
319
+ if (entry.preview.toLowerCase().includes(searchTerm)) {
320
+ return true;
321
+ }
322
+ return false;
323
+ }
324
+ // ============================================================================
325
+ // Smart Ranking
326
+ // ============================================================================
327
+ const RECENCY_WEIGHT = 0.6;
328
+ const HITS_WEIGHT = 0.4;
329
+ const RECENCY_DECAY_DAYS = 30; // Entries older than this get minimal recency score
330
+ function rankEntries(entries) {
331
+ if (entries.length === 0)
332
+ return entries;
333
+ const now = Date.now();
334
+ // Find max hits for normalization (avoid divide by zero)
335
+ const maxHits = Math.max(1, ...entries.map(e => e.hits));
336
+ // Calculate scores
337
+ const scored = entries.map(entry => {
338
+ // Recency score: 1.0 for today, decays over RECENCY_DECAY_DAYS
339
+ const ageMs = now - new Date(entry.created).getTime();
340
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
341
+ const recencyScore = Math.max(0, 1 - (ageDays / RECENCY_DECAY_DAYS));
342
+ // Hits score: normalized 0-1 against max hits in library
343
+ const hitsScore = entry.hits / maxHits;
344
+ // Blended score
345
+ const score = (RECENCY_WEIGHT * recencyScore) + (HITS_WEIGHT * hitsScore);
346
+ return { entry, score };
347
+ });
348
+ // Sort by score descending
349
+ scored.sort((a, b) => b.score - a.score);
350
+ return scored.map(s => s.entry);
351
+ }
352
+ async function fetchCloudContent(query, limit) {
353
+ try {
354
+ // Check if authenticated
355
+ const apiKey = await loadApiKey();
356
+ if (!apiKey) {
357
+ return { entries: [], total: 0 };
358
+ }
359
+ const response = await fetch(`${TELVOK_API_URL}/api/library/query`, {
360
+ method: 'POST',
361
+ headers: {
362
+ 'Content-Type': 'application/json',
363
+ 'Authorization': `Bearer ${apiKey}`,
364
+ },
365
+ body: JSON.stringify({ query, limit }),
366
+ });
367
+ if (!response.ok) {
368
+ // Don't fail if cloud query fails
369
+ return { entries: [], total: 0 };
370
+ }
371
+ const data = await response.json();
372
+ return {
373
+ entries: data.entries || [],
374
+ total: data.total || 0,
375
+ };
376
+ }
377
+ catch {
378
+ // Network error - silently return empty results
379
+ return { entries: [], total: 0 };
380
+ }
381
+ }
382
+ // ============================================================================
383
+ // Marketplace Search
384
+ // ============================================================================
385
+ async function fetchMarketplaceResults(query, limit) {
386
+ try {
387
+ const response = await fetch(`${TELVOK_API_URL}/api/search`, {
388
+ method: 'POST',
389
+ headers: { 'Content-Type': 'application/json' },
390
+ body: JSON.stringify({ query, limit }),
391
+ });
392
+ if (!response.ok) {
393
+ // Don't fail the whole brief() if marketplace is down
394
+ return { books: [], total: 0 };
395
+ }
396
+ const data = await response.json();
397
+ const books = (data.books || []).map((b) => ({
398
+ slug: b.slug,
399
+ name: b.name,
400
+ description: b.description,
401
+ pricing: b.pricing,
402
+ price: b.price,
403
+ entries: b.entries,
404
+ }));
405
+ return {
406
+ books,
407
+ total: data.total || 0,
408
+ };
409
+ }
410
+ catch {
411
+ // Network error - silently return empty results
412
+ return { books: [], total: 0 };
413
+ }
414
+ }