@usewhisper/mcp-server 1.0.0 → 1.4.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/README.md +47 -3
- package/dist/server.js +442 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Whisper MCP is the universal context bridge for coding agents. It connects Claud
|
|
|
5
5
|
## What's New (Major)
|
|
6
6
|
|
|
7
7
|
- Canonical namespaced tool surface (breaking rename)
|
|
8
|
-
- Source multiplexer in `context.add_source` for `github|web|pdf|local|slack`
|
|
8
|
+
- Source multiplexer in `context.add_source` for `github|web|pdf|local|slack|video`
|
|
9
9
|
- Auto-index by default for created sources
|
|
10
10
|
- Runtime modes: `remote` (default), `local`, `auto`
|
|
11
11
|
- Degraded retrieval behavior: lexical fallback with explicit `degraded_mode`
|
|
@@ -25,6 +25,19 @@ npm i -g @usewhisper/mcp-server
|
|
|
25
25
|
- `WHISPER_BASE_URL` (optional, defaults to `https://context.usewhisper.dev`)
|
|
26
26
|
- `WHISPER_MCP_MODE` (optional: `remote|local|auto`, default `remote`)
|
|
27
27
|
- `WHISPER_LOCAL_ALLOWLIST` (optional comma-separated roots for local ingest)
|
|
28
|
+
- `SLACK_BOT_TOKEN` (required for Slack connector runs)
|
|
29
|
+
- `SLACK_CHANNEL_ID` (required for Slack connector runs)
|
|
30
|
+
|
|
31
|
+
## Connector Status + Credentials
|
|
32
|
+
|
|
33
|
+
| Connector | Status | Minimum required config/creds |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| GitHub | Production-ready | `type=github`, `owner`, `repo` (optional `token`, `branch`, `paths`) |
|
|
36
|
+
| Web | Production-ready | `type=web`, `url` (optional `crawl_depth`, `include_paths`, `exclude_paths`) |
|
|
37
|
+
| PDF | Production-ready | `type=pdf`, `url` or `file_path` |
|
|
38
|
+
| Slack | Production-ready (requires Slack auth) | `type=slack`, `token`, `channel_ids[]` (optional `since`, `workspace_id`) |
|
|
39
|
+
| Local | Production-ready | `type=local`, `path` (optional `glob`, `max_files`) |
|
|
40
|
+
| Video | Production-ready | `type=video`, `url` (optional `platform`, `language`, `allow_stt_fallback`, `max_duration_minutes`, `max_chunks`) |
|
|
28
41
|
|
|
29
42
|
## Canonical Tools
|
|
30
43
|
|
|
@@ -52,6 +65,21 @@ npm i -g @usewhisper/mcp-server
|
|
|
52
65
|
22. `code.search_text`
|
|
53
66
|
23. `code.search_semantic`
|
|
54
67
|
|
|
68
|
+
## Agent-Friendly Aliases
|
|
69
|
+
|
|
70
|
+
These aliases sit on top of the canonical namespaced tools and are easier for coding agents to pick correctly:
|
|
71
|
+
|
|
72
|
+
- `search` -> `context.query`
|
|
73
|
+
- `search_code` -> `code.search_semantic`
|
|
74
|
+
- `grep` -> `code.search_text`
|
|
75
|
+
- `read` -> local file read with optional line ranges
|
|
76
|
+
- `explore` -> local repository tree browsing
|
|
77
|
+
- `research` -> `research.oracle`
|
|
78
|
+
- `index` -> source add or workspace refresh
|
|
79
|
+
- `remember` -> `memory.add`
|
|
80
|
+
- `recall` -> `memory.search`
|
|
81
|
+
- `share_context` -> `context.share`
|
|
82
|
+
|
|
55
83
|
## Source Contract (`context.add_source`)
|
|
56
84
|
|
|
57
85
|
Input:
|
|
@@ -60,7 +88,8 @@ Input:
|
|
|
60
88
|
- `type=web`: `url`, `crawl_depth?`, `include_paths?`, `exclude_paths?`
|
|
61
89
|
- `type=pdf`: `url?`, `file_path?`
|
|
62
90
|
- `type=local`: `path`, `glob?`, `max_files?`
|
|
63
|
-
- `type=slack`: `workspace_id?`, `
|
|
91
|
+
- `type=slack`: `token`, `channel_ids[]`, `workspace_id?`, `since?`, `auth_ref?`
|
|
92
|
+
- `type=video`: `url`, `platform?`, `language?`, `allow_stt_fallback?`, `max_duration_minutes?`, `max_chunks?`
|
|
64
93
|
|
|
65
94
|
Output:
|
|
66
95
|
- `source_id`
|
|
@@ -98,6 +127,21 @@ Local ingest (`index.local_scan_ingest` and `type=local`) enforces:
|
|
|
98
127
|
- secret/sensitive path filters (`.env`, `.pem`, `.key`, `.aws`, `.ssh`, build artifacts)
|
|
99
128
|
- content redaction pass for likely secrets
|
|
100
129
|
|
|
130
|
+
## 30-Second Demo
|
|
131
|
+
|
|
132
|
+
One-command wizard + production MCP connectors + scoped config:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx whisper-wizard
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Flow:
|
|
139
|
+
1. Run wizard and complete auth/project setup.
|
|
140
|
+
2. Add a source with MCP:
|
|
141
|
+
`context.add_source` (for example GitHub/Web/PDF/Slack/Local/Video).
|
|
142
|
+
3. Ask a grounded question:
|
|
143
|
+
`context.query` or `context.evidence_answer` for citation-locked output.
|
|
144
|
+
|
|
101
145
|
## License
|
|
102
146
|
|
|
103
|
-
MIT
|
|
147
|
+
MIT
|
package/dist/server.js
CHANGED
|
@@ -1365,6 +1365,18 @@ var TOOL_MIGRATION_MAP = [
|
|
|
1365
1365
|
{ old: "search_files", next: "code.search_text" },
|
|
1366
1366
|
{ old: "semantic_search_codebase", next: "code.search_semantic" }
|
|
1367
1367
|
];
|
|
1368
|
+
var ALIAS_TOOL_MAP = [
|
|
1369
|
+
{ alias: "search", target: "context.query" },
|
|
1370
|
+
{ alias: "search_code", target: "code.search_semantic" },
|
|
1371
|
+
{ alias: "grep", target: "code.search_text" },
|
|
1372
|
+
{ alias: "read", target: "local.file_read" },
|
|
1373
|
+
{ alias: "explore", target: "local.tree" },
|
|
1374
|
+
{ alias: "research", target: "research.oracle" },
|
|
1375
|
+
{ alias: "index", target: "context.add_source | index.workspace_run" },
|
|
1376
|
+
{ alias: "remember", target: "memory.add" },
|
|
1377
|
+
{ alias: "recall", target: "memory.search" },
|
|
1378
|
+
{ alias: "share_context", target: "context.share" }
|
|
1379
|
+
];
|
|
1368
1380
|
function ensureStateDir() {
|
|
1369
1381
|
if (!existsSync(STATE_DIR)) {
|
|
1370
1382
|
mkdirSync(STATE_DIR, { recursive: true });
|
|
@@ -1715,7 +1727,7 @@ async function ingestLocalPath(params) {
|
|
|
1715
1727
|
return { ingested, scanned: files.length, queued: docs.length, skipped, workspace_id: workspaceId };
|
|
1716
1728
|
}
|
|
1717
1729
|
async function createSourceByType(params) {
|
|
1718
|
-
const connector_type = params.type === "github" ? "github" : params.type === "web" ? "website" : params.type === "pdf" ? "pdf" : params.type === "local" ? "local-folder" : "slack";
|
|
1730
|
+
const connector_type = params.type === "github" ? "github" : params.type === "web" ? "website" : params.type === "pdf" ? "pdf" : params.type === "local" ? "local-folder" : params.type === "slack" ? "slack" : "video";
|
|
1719
1731
|
const config = {};
|
|
1720
1732
|
if (params.type === "github") {
|
|
1721
1733
|
if (!params.owner || !params.repo) throw new Error("github source requires owner and repo");
|
|
@@ -1755,6 +1767,14 @@ async function createSourceByType(params) {
|
|
|
1755
1767
|
if (params.workspace_id) config.workspace_id = params.workspace_id;
|
|
1756
1768
|
if (params.token) config.token = params.token;
|
|
1757
1769
|
if (params.auth_ref) config.auth_ref = params.auth_ref;
|
|
1770
|
+
} else if (params.type === "video") {
|
|
1771
|
+
if (!params.url) throw new Error("video source requires url");
|
|
1772
|
+
config.url = params.url;
|
|
1773
|
+
if (params.platform) config.platform = params.platform;
|
|
1774
|
+
if (params.language) config.language = params.language;
|
|
1775
|
+
if (params.allow_stt_fallback !== void 0) config.allow_stt_fallback = params.allow_stt_fallback;
|
|
1776
|
+
if (params.max_duration_minutes !== void 0) config.max_duration_minutes = params.max_duration_minutes;
|
|
1777
|
+
if (params.max_chunks !== void 0) config.max_chunks = params.max_chunks;
|
|
1758
1778
|
}
|
|
1759
1779
|
if (params.metadata) config.metadata = params.metadata;
|
|
1760
1780
|
config.auto_index = params.auto_index ?? true;
|
|
@@ -1798,6 +1818,10 @@ function printToolMap() {
|
|
|
1798
1818
|
for (const row of TOOL_MIGRATION_MAP) {
|
|
1799
1819
|
console.log(`- ${row.old} => ${row.next}`);
|
|
1800
1820
|
}
|
|
1821
|
+
console.log("\nAgent-friendly aliases:");
|
|
1822
|
+
for (const row of ALIAS_TOOL_MAP) {
|
|
1823
|
+
console.log(`- ${row.alias} => ${row.target}`);
|
|
1824
|
+
}
|
|
1801
1825
|
}
|
|
1802
1826
|
server.tool(
|
|
1803
1827
|
"index.workspace_resolve",
|
|
@@ -2309,7 +2333,7 @@ server.tool(
|
|
|
2309
2333
|
"Add a source to a project with normalized source contract and auto-index by default.",
|
|
2310
2334
|
{
|
|
2311
2335
|
project: z.string().optional().describe("Project name or slug"),
|
|
2312
|
-
type: z.enum(["github", "web", "pdf", "local", "slack"]).default("github"),
|
|
2336
|
+
type: z.enum(["github", "web", "pdf", "local", "slack", "video"]).default("github"),
|
|
2313
2337
|
name: z.string().optional(),
|
|
2314
2338
|
auto_index: z.boolean().optional().default(true),
|
|
2315
2339
|
metadata: z.record(z.string()).optional(),
|
|
@@ -2329,7 +2353,12 @@ server.tool(
|
|
|
2329
2353
|
channel_ids: z.array(z.string()).optional(),
|
|
2330
2354
|
since: z.string().optional(),
|
|
2331
2355
|
token: z.string().optional(),
|
|
2332
|
-
auth_ref: z.string().optional()
|
|
2356
|
+
auth_ref: z.string().optional(),
|
|
2357
|
+
platform: z.enum(["youtube", "loom", "generic"]).optional(),
|
|
2358
|
+
language: z.string().optional(),
|
|
2359
|
+
allow_stt_fallback: z.boolean().optional(),
|
|
2360
|
+
max_duration_minutes: z.number().optional(),
|
|
2361
|
+
max_chunks: z.number().optional()
|
|
2333
2362
|
},
|
|
2334
2363
|
async (input) => {
|
|
2335
2364
|
try {
|
|
@@ -2359,7 +2388,12 @@ server.tool(
|
|
|
2359
2388
|
channel_ids: input.channel_ids,
|
|
2360
2389
|
since: input.since,
|
|
2361
2390
|
token: input.token,
|
|
2362
|
-
auth_ref: input.auth_ref
|
|
2391
|
+
auth_ref: input.auth_ref,
|
|
2392
|
+
platform: input.platform,
|
|
2393
|
+
language: input.language,
|
|
2394
|
+
allow_stt_fallback: input.allow_stt_fallback,
|
|
2395
|
+
max_duration_minutes: input.max_duration_minutes,
|
|
2396
|
+
max_chunks: input.max_chunks
|
|
2363
2397
|
});
|
|
2364
2398
|
return toTextResult(result);
|
|
2365
2399
|
} catch (error) {
|
|
@@ -3208,6 +3242,39 @@ function* walkDir(dir, fileTypes) {
|
|
|
3208
3242
|
}
|
|
3209
3243
|
}
|
|
3210
3244
|
}
|
|
3245
|
+
function listTree(rootPath, maxDepth = 3, maxEntries = 200) {
|
|
3246
|
+
const lines = [];
|
|
3247
|
+
let seen = 0;
|
|
3248
|
+
function walk(dir, depth) {
|
|
3249
|
+
if (seen >= maxEntries || depth > maxDepth) return;
|
|
3250
|
+
let entries;
|
|
3251
|
+
try {
|
|
3252
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3253
|
+
} catch {
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
const filtered = entries.filter((entry) => !SKIP_DIRS.has(entry.name)).sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
|
|
3257
|
+
for (const entry of filtered) {
|
|
3258
|
+
if (seen >= maxEntries) return;
|
|
3259
|
+
const indent = " ".repeat(depth);
|
|
3260
|
+
lines.push(`${indent}${entry.isDirectory() ? "d" : "f"} ${entry.name}`);
|
|
3261
|
+
seen += 1;
|
|
3262
|
+
if (entry.isDirectory()) {
|
|
3263
|
+
walk(join(dir, entry.name), depth + 1);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
walk(rootPath, 0);
|
|
3268
|
+
return lines;
|
|
3269
|
+
}
|
|
3270
|
+
function readFileWindow(filePath, startLine = 1, endLine = 200) {
|
|
3271
|
+
const content = readFileSync(filePath, "utf-8");
|
|
3272
|
+
const lines = content.split("\n");
|
|
3273
|
+
const safeStart = Math.max(1, startLine);
|
|
3274
|
+
const safeEnd = Math.max(safeStart, endLine);
|
|
3275
|
+
const excerpt = lines.slice(safeStart - 1, safeEnd).map((line, index) => `${safeStart + index}: ${line}`).join("\n");
|
|
3276
|
+
return excerpt || "(empty file)";
|
|
3277
|
+
}
|
|
3211
3278
|
server.tool(
|
|
3212
3279
|
"code.search_text",
|
|
3213
3280
|
"Search files and content in a local directory without requiring pre-indexing. Uses ripgrep when available, falls back to Node.js. Great for finding files, functions, patterns, or any text across a codebase instantly.",
|
|
@@ -3391,6 +3458,377 @@ server.tool(
|
|
|
3391
3458
|
}
|
|
3392
3459
|
}
|
|
3393
3460
|
);
|
|
3461
|
+
server.tool(
|
|
3462
|
+
"search",
|
|
3463
|
+
"Search indexed code, docs, and connected sources with one obvious verb. Use this first for most retrieval tasks.",
|
|
3464
|
+
{
|
|
3465
|
+
project: z.string().optional().describe("Project name or slug"),
|
|
3466
|
+
query: z.string().describe("What you want to find"),
|
|
3467
|
+
top_k: z.number().optional().default(10),
|
|
3468
|
+
include_memories: z.boolean().optional().default(false),
|
|
3469
|
+
include_graph: z.boolean().optional().default(false),
|
|
3470
|
+
user_id: z.string().optional(),
|
|
3471
|
+
session_id: z.string().optional()
|
|
3472
|
+
},
|
|
3473
|
+
async ({ project, query, top_k, include_memories, include_graph, user_id, session_id }) => {
|
|
3474
|
+
try {
|
|
3475
|
+
const resolvedProject = await resolveProjectRef(project);
|
|
3476
|
+
if (!resolvedProject) {
|
|
3477
|
+
return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
|
|
3478
|
+
}
|
|
3479
|
+
const queryResult = await queryWithDegradedFallback({
|
|
3480
|
+
project: resolvedProject,
|
|
3481
|
+
query,
|
|
3482
|
+
top_k,
|
|
3483
|
+
include_memories,
|
|
3484
|
+
include_graph,
|
|
3485
|
+
user_id,
|
|
3486
|
+
session_id
|
|
3487
|
+
});
|
|
3488
|
+
const response = queryResult.response;
|
|
3489
|
+
if (!response.results?.length) {
|
|
3490
|
+
return { content: [{ type: "text", text: `No relevant results found for "${query}".` }] };
|
|
3491
|
+
}
|
|
3492
|
+
const suffix = queryResult.degraded_mode ? `
|
|
3493
|
+
|
|
3494
|
+
[degraded_mode=true] ${queryResult.degraded_reason}` : "";
|
|
3495
|
+
return { content: [{ type: "text", text: response.context + suffix }] };
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
);
|
|
3501
|
+
server.tool(
|
|
3502
|
+
"search_code",
|
|
3503
|
+
"Semantically search a local codebase by meaning. Use this for questions like 'where is auth handled?' or 'find retry logic'.",
|
|
3504
|
+
{
|
|
3505
|
+
query: z.string().describe("Natural-language code search query"),
|
|
3506
|
+
path: z.string().optional().describe("Codebase root. Defaults to current working directory."),
|
|
3507
|
+
file_types: z.array(z.string()).optional(),
|
|
3508
|
+
top_k: z.number().optional().default(10),
|
|
3509
|
+
threshold: z.number().optional().default(0.2),
|
|
3510
|
+
max_files: z.number().optional().default(150)
|
|
3511
|
+
},
|
|
3512
|
+
async ({ query, path, file_types, top_k, threshold, max_files }) => {
|
|
3513
|
+
const rootPath = path || process.cwd();
|
|
3514
|
+
const allowedExts = file_types ? new Set(file_types) : CODE_EXTENSIONS;
|
|
3515
|
+
const files = [];
|
|
3516
|
+
function collect(dir) {
|
|
3517
|
+
if (files.length >= (max_files ?? 150)) return;
|
|
3518
|
+
let entries;
|
|
3519
|
+
try {
|
|
3520
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
3521
|
+
} catch {
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
for (const entry of entries) {
|
|
3525
|
+
if (files.length >= (max_files ?? 150)) break;
|
|
3526
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
3527
|
+
const full = join(dir, entry.name);
|
|
3528
|
+
if (entry.isDirectory()) collect(full);
|
|
3529
|
+
else if (entry.isFile()) {
|
|
3530
|
+
const ext = extname(entry.name).replace(".", "");
|
|
3531
|
+
if (allowedExts.has(ext)) files.push(full);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
collect(rootPath);
|
|
3536
|
+
if (files.length === 0) {
|
|
3537
|
+
return { content: [{ type: "text", text: `No code files found in ${rootPath}` }] };
|
|
3538
|
+
}
|
|
3539
|
+
const documents = [];
|
|
3540
|
+
for (const filePath of files) {
|
|
3541
|
+
try {
|
|
3542
|
+
const stat = statSync(filePath);
|
|
3543
|
+
if (stat.size > 500 * 1024) continue;
|
|
3544
|
+
const content = readFileSync(filePath, "utf-8");
|
|
3545
|
+
const relPath = relative(rootPath, filePath);
|
|
3546
|
+
documents.push({ id: relPath, content: extractSignature(relPath, content) });
|
|
3547
|
+
} catch {
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
try {
|
|
3551
|
+
const response = await whisper.semanticSearch({
|
|
3552
|
+
query,
|
|
3553
|
+
documents,
|
|
3554
|
+
top_k: top_k ?? 10,
|
|
3555
|
+
threshold: threshold ?? 0.2
|
|
3556
|
+
});
|
|
3557
|
+
if (!response.results?.length) {
|
|
3558
|
+
return { content: [{ type: "text", text: `No semantically relevant files found for "${query}".` }] };
|
|
3559
|
+
}
|
|
3560
|
+
const lines = response.results.map((result) => `${result.id} (score: ${result.score})${result.snippet ? `
|
|
3561
|
+
${result.snippet}` : ""}`);
|
|
3562
|
+
return { content: [{ type: "text", text: lines.join("\n\n") }] };
|
|
3563
|
+
} catch (error) {
|
|
3564
|
+
return { content: [{ type: "text", text: `Semantic search failed: ${error.message}` }] };
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
);
|
|
3568
|
+
server.tool(
|
|
3569
|
+
"grep",
|
|
3570
|
+
"Regex or text search across a local codebase. Use this when you know the symbol, string, or pattern you want.",
|
|
3571
|
+
{
|
|
3572
|
+
query: z.string().describe("Text or regex-like pattern to search for"),
|
|
3573
|
+
path: z.string().optional().describe("Search root. Defaults to current working directory."),
|
|
3574
|
+
file_types: z.array(z.string()).optional(),
|
|
3575
|
+
max_results: z.number().optional().default(20),
|
|
3576
|
+
case_sensitive: z.boolean().optional().default(false)
|
|
3577
|
+
},
|
|
3578
|
+
async ({ query, path, file_types, max_results, case_sensitive }) => {
|
|
3579
|
+
const rootPath = path || process.cwd();
|
|
3580
|
+
const results = [];
|
|
3581
|
+
const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), case_sensitive ? "g" : "gi");
|
|
3582
|
+
for (const filePath of walkDir(rootPath, file_types)) {
|
|
3583
|
+
if (results.length >= (max_results ?? 20)) break;
|
|
3584
|
+
try {
|
|
3585
|
+
const stat = statSync(filePath);
|
|
3586
|
+
if (stat.size > 512 * 1024) continue;
|
|
3587
|
+
const text = readFileSync(filePath, "utf-8");
|
|
3588
|
+
const lines2 = text.split("\n");
|
|
3589
|
+
const matches = [];
|
|
3590
|
+
lines2.forEach((line, index) => {
|
|
3591
|
+
regex.lastIndex = 0;
|
|
3592
|
+
if (regex.test(line)) {
|
|
3593
|
+
matches.push({ line: index + 1, content: line.trimEnd() });
|
|
3594
|
+
}
|
|
3595
|
+
});
|
|
3596
|
+
if (matches.length > 0) {
|
|
3597
|
+
results.push({ file: relative(rootPath, filePath), matches: matches.slice(0, 10) });
|
|
3598
|
+
}
|
|
3599
|
+
} catch {
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
if (!results.length) {
|
|
3603
|
+
return { content: [{ type: "text", text: `No matches found for "${query}" in ${rootPath}` }] };
|
|
3604
|
+
}
|
|
3605
|
+
const lines = results.flatMap((result) => [
|
|
3606
|
+
`FILE ${result.file}`,
|
|
3607
|
+
...result.matches.map((match) => `L${match.line}: ${match.content}`),
|
|
3608
|
+
""
|
|
3609
|
+
]);
|
|
3610
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
3611
|
+
}
|
|
3612
|
+
);
|
|
3613
|
+
server.tool(
|
|
3614
|
+
"read",
|
|
3615
|
+
"Read a local file with optional line ranges. Use this after search or grep when you want the actual source.",
|
|
3616
|
+
{
|
|
3617
|
+
path: z.string().describe("Absolute or relative path to the file to read."),
|
|
3618
|
+
start_line: z.number().optional().default(1),
|
|
3619
|
+
end_line: z.number().optional().default(200)
|
|
3620
|
+
},
|
|
3621
|
+
async ({ path, start_line, end_line }) => {
|
|
3622
|
+
try {
|
|
3623
|
+
const fullPath = path.includes(":") || path.startsWith("/") ? path : join(process.cwd(), path);
|
|
3624
|
+
const stats = statSync(fullPath);
|
|
3625
|
+
if (!stats.isFile()) {
|
|
3626
|
+
return { content: [{ type: "text", text: `Error: ${path} is not a file.` }] };
|
|
3627
|
+
}
|
|
3628
|
+
return { content: [{ type: "text", text: readFileWindow(fullPath, start_line, end_line) }] };
|
|
3629
|
+
} catch (error) {
|
|
3630
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
);
|
|
3634
|
+
server.tool(
|
|
3635
|
+
"explore",
|
|
3636
|
+
"Browse a repository tree or directory structure. Use this to orient yourself before reading files.",
|
|
3637
|
+
{
|
|
3638
|
+
path: z.string().optional().describe("Root directory to inspect. Defaults to current working directory."),
|
|
3639
|
+
max_depth: z.number().optional().default(3),
|
|
3640
|
+
max_entries: z.number().optional().default(200)
|
|
3641
|
+
},
|
|
3642
|
+
async ({ path, max_depth, max_entries }) => {
|
|
3643
|
+
try {
|
|
3644
|
+
const rootPath = path || process.cwd();
|
|
3645
|
+
const tree = listTree(rootPath, max_depth, max_entries);
|
|
3646
|
+
if (!tree.length) {
|
|
3647
|
+
return { content: [{ type: "text", text: `No visible files found in ${rootPath}` }] };
|
|
3648
|
+
}
|
|
3649
|
+
return { content: [{ type: "text", text: [`TREE ${rootPath}`, ...tree].join("\n") }] };
|
|
3650
|
+
} catch (error) {
|
|
3651
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
);
|
|
3655
|
+
server.tool(
|
|
3656
|
+
"research",
|
|
3657
|
+
"Run deeper research over indexed sources. Use this when search is not enough and you want synthesis or multi-step investigation.",
|
|
3658
|
+
{
|
|
3659
|
+
project: z.string().optional(),
|
|
3660
|
+
query: z.string().describe("Research question"),
|
|
3661
|
+
mode: z.enum(["search", "research"]).optional().default("research"),
|
|
3662
|
+
max_results: z.number().optional().default(5),
|
|
3663
|
+
max_steps: z.number().optional().default(5)
|
|
3664
|
+
},
|
|
3665
|
+
async ({ project, query, mode, max_results, max_steps }) => {
|
|
3666
|
+
try {
|
|
3667
|
+
const results = await whisper.oracleSearch({ project, query, mode, max_results, max_steps });
|
|
3668
|
+
if (mode === "research" && results.answer) {
|
|
3669
|
+
return { content: [{ type: "text", text: results.answer }] };
|
|
3670
|
+
}
|
|
3671
|
+
if (!results.results?.length) {
|
|
3672
|
+
return { content: [{ type: "text", text: "No research results found." }] };
|
|
3673
|
+
}
|
|
3674
|
+
const text = results.results.map((r, i) => `${i + 1}. ${r.path || r.source}
|
|
3675
|
+
${String(r.content || "").slice(0, 200)}...`).join("\n\n");
|
|
3676
|
+
return { content: [{ type: "text", text }] };
|
|
3677
|
+
} catch (error) {
|
|
3678
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
);
|
|
3682
|
+
server.tool(
|
|
3683
|
+
"index",
|
|
3684
|
+
"Index a new source or refresh a workspace. Use action='source' to add GitHub/web/pdf/local/slack/video. Use action='workspace' to refresh local workspace metadata.",
|
|
3685
|
+
{
|
|
3686
|
+
action: z.enum(["source", "workspace"]).default("source"),
|
|
3687
|
+
project: z.string().optional(),
|
|
3688
|
+
type: z.enum(["github", "web", "pdf", "local", "slack", "video"]).optional(),
|
|
3689
|
+
name: z.string().optional(),
|
|
3690
|
+
owner: z.string().optional(),
|
|
3691
|
+
repo: z.string().optional(),
|
|
3692
|
+
branch: z.string().optional(),
|
|
3693
|
+
url: z.string().optional(),
|
|
3694
|
+
file_path: z.string().optional(),
|
|
3695
|
+
path: z.string().optional(),
|
|
3696
|
+
glob: z.string().optional(),
|
|
3697
|
+
max_files: z.number().optional(),
|
|
3698
|
+
channel_ids: z.array(z.string()).optional(),
|
|
3699
|
+
token: z.string().optional(),
|
|
3700
|
+
workspace_id: z.string().optional(),
|
|
3701
|
+
mode: z.enum(["full", "incremental"]).optional().default("incremental"),
|
|
3702
|
+
platform: z.enum(["youtube", "loom", "generic"]).optional(),
|
|
3703
|
+
language: z.string().optional(),
|
|
3704
|
+
allow_stt_fallback: z.boolean().optional(),
|
|
3705
|
+
max_duration_minutes: z.number().optional(),
|
|
3706
|
+
max_chunks: z.number().optional()
|
|
3707
|
+
},
|
|
3708
|
+
async (input) => {
|
|
3709
|
+
try {
|
|
3710
|
+
if (input.action === "workspace") {
|
|
3711
|
+
const rootPath = input.path || process.cwd();
|
|
3712
|
+
const workspaceId = getWorkspaceIdForPath(rootPath, input.workspace_id);
|
|
3713
|
+
const state = loadState();
|
|
3714
|
+
const workspace = getWorkspaceState(state, workspaceId);
|
|
3715
|
+
const fileStats = countCodeFiles(rootPath, input.max_files || 1500);
|
|
3716
|
+
workspace.index_metadata = {
|
|
3717
|
+
last_indexed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3718
|
+
last_indexed_commit: getGitHead(rootPath),
|
|
3719
|
+
coverage: input.mode === "full" ? 1 : Math.max(0, Math.min(1, fileStats.total / Math.max(1, input.max_files || 1500)))
|
|
3720
|
+
};
|
|
3721
|
+
saveState(state);
|
|
3722
|
+
return toTextResult({ workspace_id: workspaceId, mode: input.mode, indexed_files: fileStats.total, skipped_files: fileStats.skipped, index_metadata: workspace.index_metadata });
|
|
3723
|
+
}
|
|
3724
|
+
if (!input.type) {
|
|
3725
|
+
return { content: [{ type: "text", text: "Error: type is required when action='source'." }] };
|
|
3726
|
+
}
|
|
3727
|
+
const resolvedProject = await resolveProjectRef(input.project);
|
|
3728
|
+
if (!resolvedProject) {
|
|
3729
|
+
return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
|
|
3730
|
+
}
|
|
3731
|
+
const result = await createSourceByType({
|
|
3732
|
+
project: resolvedProject,
|
|
3733
|
+
type: input.type,
|
|
3734
|
+
name: input.name,
|
|
3735
|
+
owner: input.owner,
|
|
3736
|
+
repo: input.repo,
|
|
3737
|
+
branch: input.branch,
|
|
3738
|
+
url: input.url,
|
|
3739
|
+
file_path: input.file_path,
|
|
3740
|
+
path: input.path,
|
|
3741
|
+
glob: input.glob,
|
|
3742
|
+
max_files: input.max_files,
|
|
3743
|
+
channel_ids: input.channel_ids,
|
|
3744
|
+
token: input.token,
|
|
3745
|
+
workspace_id: input.workspace_id,
|
|
3746
|
+
platform: input.platform,
|
|
3747
|
+
language: input.language,
|
|
3748
|
+
allow_stt_fallback: input.allow_stt_fallback,
|
|
3749
|
+
max_duration_minutes: input.max_duration_minutes,
|
|
3750
|
+
max_chunks: input.max_chunks,
|
|
3751
|
+
auto_index: true
|
|
3752
|
+
});
|
|
3753
|
+
return toTextResult(result);
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
);
|
|
3759
|
+
server.tool(
|
|
3760
|
+
"remember",
|
|
3761
|
+
"Store something the agent should keep across sessions: a fact, decision, preference, or instruction.",
|
|
3762
|
+
{
|
|
3763
|
+
project: z.string().optional(),
|
|
3764
|
+
content: z.string().describe("Memory content"),
|
|
3765
|
+
memory_type: z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"]).optional().default("factual"),
|
|
3766
|
+
user_id: z.string().optional(),
|
|
3767
|
+
session_id: z.string().optional(),
|
|
3768
|
+
agent_id: z.string().optional(),
|
|
3769
|
+
importance: z.number().optional().default(0.5)
|
|
3770
|
+
},
|
|
3771
|
+
async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
|
|
3772
|
+
try {
|
|
3773
|
+
const result = await whisper.addMemory({ project, content, memory_type, user_id, session_id, agent_id, importance });
|
|
3774
|
+
return { content: [{ type: "text", text: `Memory stored (id: ${result.id}, type: ${memory_type}).` }] };
|
|
3775
|
+
} catch (error) {
|
|
3776
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
);
|
|
3780
|
+
server.tool(
|
|
3781
|
+
"recall",
|
|
3782
|
+
"Recall facts, decisions, and preferences from previous sessions or prior work.",
|
|
3783
|
+
{
|
|
3784
|
+
project: z.string().optional(),
|
|
3785
|
+
query: z.string().describe("What to recall"),
|
|
3786
|
+
user_id: z.string().optional(),
|
|
3787
|
+
session_id: z.string().optional(),
|
|
3788
|
+
top_k: z.number().optional().default(10),
|
|
3789
|
+
memory_types: z.array(z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"])).optional()
|
|
3790
|
+
},
|
|
3791
|
+
async ({ project, query, user_id, session_id, top_k, memory_types }) => {
|
|
3792
|
+
try {
|
|
3793
|
+
const results = await whisper.searchMemoriesSOTA({ project, query, user_id, session_id, top_k, memory_types });
|
|
3794
|
+
if (!results.memories?.length) {
|
|
3795
|
+
return { content: [{ type: "text", text: "No memories found." }] };
|
|
3796
|
+
}
|
|
3797
|
+
const text = results.memories.map((r, i) => `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
|
|
3798
|
+
${r.content}`).join("\n\n");
|
|
3799
|
+
return { content: [{ type: "text", text }] };
|
|
3800
|
+
} catch (error) {
|
|
3801
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
);
|
|
3805
|
+
server.tool(
|
|
3806
|
+
"share_context",
|
|
3807
|
+
"Create a shareable snapshot of a session so another agent or teammate can continue with the same context.",
|
|
3808
|
+
{
|
|
3809
|
+
project: z.string().optional(),
|
|
3810
|
+
session_id: z.string().describe("Session to share"),
|
|
3811
|
+
title: z.string().optional(),
|
|
3812
|
+
expiry_days: z.number().optional().default(30)
|
|
3813
|
+
},
|
|
3814
|
+
async ({ project, session_id, title, expiry_days }) => {
|
|
3815
|
+
try {
|
|
3816
|
+
const result = await whisper.createSharedContext({ project, session_id, title, expiry_days });
|
|
3817
|
+
return {
|
|
3818
|
+
content: [{
|
|
3819
|
+
type: "text",
|
|
3820
|
+
text: `Shared context created:
|
|
3821
|
+
- Share ID: ${result.share_id}
|
|
3822
|
+
- Expires: ${result.expires_at || "Never"}
|
|
3823
|
+
|
|
3824
|
+
Share URL: ${result.share_url}`
|
|
3825
|
+
}]
|
|
3826
|
+
};
|
|
3827
|
+
} catch (error) {
|
|
3828
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }] };
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
);
|
|
3394
3832
|
async function main() {
|
|
3395
3833
|
const args = process.argv.slice(2);
|
|
3396
3834
|
if (args.includes("--print-tool-map")) {
|