@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.
- 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/sensitive-scanner.d.ts +20 -0
- package/dist/library/sensitive-scanner.js +56 -0
- 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 +104 -54
- package/dist/tools/adopt.d.ts +1 -0
- package/dist/tools/adopt.js +37 -10
- package/dist/tools/audit.d.ts +27 -0
- package/dist/tools/audit.js +126 -0
- 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 +117 -0
- package/dist/tools/library-publish.js +447 -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,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
|
+
}
|