talon-agent 1.3.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/package.json +3 -1
- package/src/__tests__/compose-tools.test.ts +216 -0
- package/src/__tests__/fuzz.test.ts +0 -2
- package/src/__tests__/gateway-actions.test.ts +1 -423
- package/src/backend/claude-sdk/index.ts +39 -54
- package/src/backend/opencode/index.ts +5 -20
- package/src/bootstrap.ts +70 -6
- package/src/core/gateway-actions.ts +0 -87
- package/src/core/tools/bridge.ts +40 -0
- package/src/core/tools/chat.ts +52 -0
- package/src/core/tools/history.ts +80 -0
- package/src/core/tools/index.ts +82 -0
- package/src/core/tools/mcp-server.ts +64 -0
- package/src/core/tools/media.ts +23 -0
- package/src/core/tools/members.ts +46 -0
- package/src/core/tools/messaging.ts +300 -0
- package/src/core/tools/scheduling.ts +89 -0
- package/src/core/tools/stickers.ts +143 -0
- package/src/core/tools/types.ts +60 -0
- package/src/core/tools/web.ts +26 -0
- package/src/frontend/telegram/actions.ts +10 -1
- package/src/plugins/github/index.ts +106 -0
- package/src/plugins/playwright/index.ts +82 -0
- package/src/storage/sessions.ts +0 -10
- package/src/util/config.ts +20 -1
- package/src/util/log.ts +3 -1
- package/src/backend/claude-sdk/tools.ts +0 -651
- package/src/frontend/teams/tools.ts +0 -175
|
@@ -125,9 +125,6 @@ describe("gateway shared actions", () => {
|
|
|
125
125
|
originalFetch = globalThis.fetch;
|
|
126
126
|
originalEnv = { ...process.env };
|
|
127
127
|
vi.clearAllMocks();
|
|
128
|
-
// Reset env vars used by web_search
|
|
129
|
-
delete process.env.TALON_BRAVE_API_KEY;
|
|
130
|
-
delete process.env.TALON_SEARXNG_URL;
|
|
131
128
|
});
|
|
132
129
|
|
|
133
130
|
afterEach(() => {
|
|
@@ -276,377 +273,6 @@ describe("gateway shared actions", () => {
|
|
|
276
273
|
});
|
|
277
274
|
});
|
|
278
275
|
|
|
279
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
280
|
-
// web_search
|
|
281
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
282
|
-
|
|
283
|
-
describe("web_search", () => {
|
|
284
|
-
it("returns error for missing query", async () => {
|
|
285
|
-
const result = await handleSharedAction({ action: "web_search" }, 123);
|
|
286
|
-
expect(result).toEqual({ ok: false, error: "Missing query" });
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("returns error for empty query string", async () => {
|
|
290
|
-
const result = await handleSharedAction(
|
|
291
|
-
{ action: "web_search", query: "" },
|
|
292
|
-
123,
|
|
293
|
-
);
|
|
294
|
-
expect(result).toEqual({ ok: false, error: "Missing query" });
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("uses Brave API when key is configured", async () => {
|
|
298
|
-
process.env.TALON_BRAVE_API_KEY = "test-brave-key";
|
|
299
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
300
|
-
mockResponse({
|
|
301
|
-
ok: true,
|
|
302
|
-
contentType: "application/json",
|
|
303
|
-
json: {
|
|
304
|
-
web: {
|
|
305
|
-
results: [
|
|
306
|
-
{
|
|
307
|
-
title: "Result 1",
|
|
308
|
-
url: "https://example.com/1",
|
|
309
|
-
description: "Description 1",
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
title: "Result 2",
|
|
313
|
-
url: "https://example.com/2",
|
|
314
|
-
description: "Description 2",
|
|
315
|
-
},
|
|
316
|
-
],
|
|
317
|
-
},
|
|
318
|
-
},
|
|
319
|
-
}),
|
|
320
|
-
);
|
|
321
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
322
|
-
|
|
323
|
-
const result = await handleSharedAction(
|
|
324
|
-
{ action: "web_search", query: "test query" },
|
|
325
|
-
123,
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
expect(result?.ok).toBe(true);
|
|
329
|
-
expect(result?.text).toContain("via Brave");
|
|
330
|
-
expect(result?.text).toContain("Result 1");
|
|
331
|
-
expect(result?.text).toContain("https://example.com/1");
|
|
332
|
-
expect(result?.text).toContain("Description 1");
|
|
333
|
-
expect(result?.text).toContain("Result 2");
|
|
334
|
-
|
|
335
|
-
// Verify Brave API was called correctly
|
|
336
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
337
|
-
const [url, opts] = mockFetch.mock.calls[0];
|
|
338
|
-
expect(url).toContain("api.search.brave.com");
|
|
339
|
-
expect(url).toContain("q=test%20query");
|
|
340
|
-
expect(url).toContain("count=5");
|
|
341
|
-
expect(opts.headers["X-Subscription-Token"]).toBe("test-brave-key");
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it("respects custom limit for Brave API", async () => {
|
|
345
|
-
process.env.TALON_BRAVE_API_KEY = "test-brave-key";
|
|
346
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
347
|
-
mockResponse({
|
|
348
|
-
ok: true,
|
|
349
|
-
json: {
|
|
350
|
-
web: {
|
|
351
|
-
results: [{ title: "R", url: "https://r.com", description: "d" }],
|
|
352
|
-
},
|
|
353
|
-
},
|
|
354
|
-
}),
|
|
355
|
-
);
|
|
356
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
357
|
-
|
|
358
|
-
await handleSharedAction(
|
|
359
|
-
{ action: "web_search", query: "test", limit: 8 },
|
|
360
|
-
123,
|
|
361
|
-
);
|
|
362
|
-
const [url] = mockFetch.mock.calls[0];
|
|
363
|
-
expect(url).toContain("count=8");
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it("clamps search limit to 10", async () => {
|
|
367
|
-
process.env.TALON_BRAVE_API_KEY = "test-brave-key";
|
|
368
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
369
|
-
mockResponse({
|
|
370
|
-
ok: true,
|
|
371
|
-
json: {
|
|
372
|
-
web: {
|
|
373
|
-
results: [{ title: "R", url: "https://r.com", description: "d" }],
|
|
374
|
-
},
|
|
375
|
-
},
|
|
376
|
-
}),
|
|
377
|
-
);
|
|
378
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
379
|
-
|
|
380
|
-
await handleSharedAction(
|
|
381
|
-
{ action: "web_search", query: "test", limit: 50 },
|
|
382
|
-
123,
|
|
383
|
-
);
|
|
384
|
-
const [url] = mockFetch.mock.calls[0];
|
|
385
|
-
expect(url).toContain("count=10");
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it("falls back to SearXNG when Brave returns non-ok", async () => {
|
|
389
|
-
process.env.TALON_BRAVE_API_KEY = "test-brave-key";
|
|
390
|
-
const mockFetch = vi
|
|
391
|
-
.fn()
|
|
392
|
-
.mockResolvedValueOnce(mockResponse({ ok: false, status: 429 })) // Brave fails
|
|
393
|
-
.mockResolvedValueOnce(
|
|
394
|
-
mockResponse({
|
|
395
|
-
ok: true,
|
|
396
|
-
json: {
|
|
397
|
-
results: [
|
|
398
|
-
{
|
|
399
|
-
title: "SearX Result",
|
|
400
|
-
url: "https://searx.example.com",
|
|
401
|
-
content: "SearX snippet",
|
|
402
|
-
},
|
|
403
|
-
],
|
|
404
|
-
},
|
|
405
|
-
}),
|
|
406
|
-
);
|
|
407
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
408
|
-
|
|
409
|
-
const result = await handleSharedAction(
|
|
410
|
-
{ action: "web_search", query: "fallback test" },
|
|
411
|
-
123,
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
expect(mockFetch).toHaveBeenCalledTimes(2);
|
|
415
|
-
expect(result?.ok).toBe(true);
|
|
416
|
-
expect(result?.text).toContain("via SearXNG");
|
|
417
|
-
expect(result?.text).toContain("SearX Result");
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
it("falls back to SearXNG when Brave throws an error", async () => {
|
|
421
|
-
process.env.TALON_BRAVE_API_KEY = "test-brave-key";
|
|
422
|
-
const mockFetch = vi
|
|
423
|
-
.fn()
|
|
424
|
-
.mockRejectedValueOnce(new Error("network error")) // Brave throws
|
|
425
|
-
.mockResolvedValueOnce(
|
|
426
|
-
mockResponse({
|
|
427
|
-
ok: true,
|
|
428
|
-
json: {
|
|
429
|
-
results: [
|
|
430
|
-
{
|
|
431
|
-
title: "Fallback",
|
|
432
|
-
url: "https://fb.com",
|
|
433
|
-
content: "snippet",
|
|
434
|
-
},
|
|
435
|
-
],
|
|
436
|
-
},
|
|
437
|
-
}),
|
|
438
|
-
);
|
|
439
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
440
|
-
|
|
441
|
-
const result = await handleSharedAction(
|
|
442
|
-
{ action: "web_search", query: "test" },
|
|
443
|
-
123,
|
|
444
|
-
);
|
|
445
|
-
|
|
446
|
-
expect(result?.ok).toBe(true);
|
|
447
|
-
expect(result?.text).toContain("via SearXNG");
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it("uses SearXNG directly when no Brave key", async () => {
|
|
451
|
-
// No TALON_BRAVE_API_KEY set
|
|
452
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
453
|
-
mockResponse({
|
|
454
|
-
ok: true,
|
|
455
|
-
json: {
|
|
456
|
-
results: [
|
|
457
|
-
{
|
|
458
|
-
title: "Direct SearX",
|
|
459
|
-
url: "https://searx.com/r",
|
|
460
|
-
content: "content here",
|
|
461
|
-
},
|
|
462
|
-
],
|
|
463
|
-
},
|
|
464
|
-
}),
|
|
465
|
-
);
|
|
466
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
467
|
-
|
|
468
|
-
const result = await handleSharedAction(
|
|
469
|
-
{ action: "web_search", query: "direct" },
|
|
470
|
-
123,
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
474
|
-
const [url] = mockFetch.mock.calls[0];
|
|
475
|
-
expect(url).toContain("localhost:8080");
|
|
476
|
-
expect(url).toContain("format=json");
|
|
477
|
-
expect(result?.ok).toBe(true);
|
|
478
|
-
expect(result?.text).toContain("via SearXNG");
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it("uses custom SearXNG URL from env", async () => {
|
|
482
|
-
process.env.TALON_SEARXNG_URL = "http://my-searx:9090";
|
|
483
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
484
|
-
mockResponse({
|
|
485
|
-
ok: true,
|
|
486
|
-
json: {
|
|
487
|
-
results: [{ title: "T", url: "https://t.com", content: "c" }],
|
|
488
|
-
},
|
|
489
|
-
}),
|
|
490
|
-
);
|
|
491
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
492
|
-
|
|
493
|
-
await handleSharedAction({ action: "web_search", query: "custom" }, 123);
|
|
494
|
-
|
|
495
|
-
const [url] = mockFetch.mock.calls[0];
|
|
496
|
-
expect(url).toContain("my-searx:9090");
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
it("returns 'no results' when both providers fail", async () => {
|
|
500
|
-
process.env.TALON_BRAVE_API_KEY = "test-key";
|
|
501
|
-
const mockFetch = vi
|
|
502
|
-
.fn()
|
|
503
|
-
.mockRejectedValueOnce(new Error("brave fail"))
|
|
504
|
-
.mockRejectedValueOnce(new Error("searx fail"));
|
|
505
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
506
|
-
|
|
507
|
-
const result = await handleSharedAction(
|
|
508
|
-
{ action: "web_search", query: "nothing" },
|
|
509
|
-
123,
|
|
510
|
-
);
|
|
511
|
-
|
|
512
|
-
expect(result?.ok).toBe(true);
|
|
513
|
-
expect(result?.text).toBe('No results for "nothing".');
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
it("returns 'no results' when both return non-ok", async () => {
|
|
517
|
-
process.env.TALON_BRAVE_API_KEY = "test-key";
|
|
518
|
-
const mockFetch = vi
|
|
519
|
-
.fn()
|
|
520
|
-
.mockResolvedValueOnce(mockResponse({ ok: false, status: 500 }))
|
|
521
|
-
.mockResolvedValueOnce(mockResponse({ ok: false, status: 503 }));
|
|
522
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
523
|
-
|
|
524
|
-
const result = await handleSharedAction(
|
|
525
|
-
{ action: "web_search", query: "failing" },
|
|
526
|
-
123,
|
|
527
|
-
);
|
|
528
|
-
|
|
529
|
-
expect(result?.ok).toBe(true);
|
|
530
|
-
expect(result?.text).toBe('No results for "failing".');
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it("returns 'no results' when Brave returns empty results array", async () => {
|
|
534
|
-
process.env.TALON_BRAVE_API_KEY = "test-key";
|
|
535
|
-
const mockFetch = vi
|
|
536
|
-
.fn()
|
|
537
|
-
.mockResolvedValueOnce(
|
|
538
|
-
mockResponse({ ok: true, json: { web: { results: [] } } }),
|
|
539
|
-
)
|
|
540
|
-
.mockResolvedValueOnce(
|
|
541
|
-
mockResponse({ ok: true, json: { results: [] } }),
|
|
542
|
-
);
|
|
543
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
544
|
-
|
|
545
|
-
const result = await handleSharedAction(
|
|
546
|
-
{ action: "web_search", query: "empty" },
|
|
547
|
-
123,
|
|
548
|
-
);
|
|
549
|
-
|
|
550
|
-
expect(result?.ok).toBe(true);
|
|
551
|
-
expect(result?.text).toBe('No results for "empty".');
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
it("handles Brave response with missing web field", async () => {
|
|
555
|
-
process.env.TALON_BRAVE_API_KEY = "test-key";
|
|
556
|
-
const mockFetch = vi
|
|
557
|
-
.fn()
|
|
558
|
-
.mockResolvedValueOnce(mockResponse({ ok: true, json: {} })) // no web field
|
|
559
|
-
.mockResolvedValueOnce(
|
|
560
|
-
mockResponse({
|
|
561
|
-
ok: true,
|
|
562
|
-
json: {
|
|
563
|
-
results: [
|
|
564
|
-
{ title: "FallbackR", url: "https://f.com", content: "fb" },
|
|
565
|
-
],
|
|
566
|
-
},
|
|
567
|
-
}),
|
|
568
|
-
);
|
|
569
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
570
|
-
|
|
571
|
-
const result = await handleSharedAction(
|
|
572
|
-
{ action: "web_search", query: "test" },
|
|
573
|
-
123,
|
|
574
|
-
);
|
|
575
|
-
expect(result?.text).toContain("via SearXNG");
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
it("truncates long snippets to 200 chars", async () => {
|
|
579
|
-
process.env.TALON_BRAVE_API_KEY = "test-key";
|
|
580
|
-
const longDesc = "A".repeat(500);
|
|
581
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
582
|
-
mockResponse({
|
|
583
|
-
ok: true,
|
|
584
|
-
json: {
|
|
585
|
-
web: {
|
|
586
|
-
results: [
|
|
587
|
-
{ title: "Long", url: "https://l.com", description: longDesc },
|
|
588
|
-
],
|
|
589
|
-
},
|
|
590
|
-
},
|
|
591
|
-
}),
|
|
592
|
-
);
|
|
593
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
594
|
-
|
|
595
|
-
const result = await handleSharedAction(
|
|
596
|
-
{ action: "web_search", query: "long" },
|
|
597
|
-
123,
|
|
598
|
-
);
|
|
599
|
-
// The snippet should be sliced to 200 chars
|
|
600
|
-
expect(result?.text).not.toContain("A".repeat(201));
|
|
601
|
-
expect(result?.text).toContain("A".repeat(200));
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
it("handles missing description in Brave results", async () => {
|
|
605
|
-
process.env.TALON_BRAVE_API_KEY = "test-key";
|
|
606
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
607
|
-
mockResponse({
|
|
608
|
-
ok: true,
|
|
609
|
-
json: {
|
|
610
|
-
web: { results: [{ title: "NoDesc", url: "https://nd.com" }] },
|
|
611
|
-
},
|
|
612
|
-
}),
|
|
613
|
-
);
|
|
614
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
615
|
-
|
|
616
|
-
const result = await handleSharedAction(
|
|
617
|
-
{ action: "web_search", query: "nodesc" },
|
|
618
|
-
123,
|
|
619
|
-
);
|
|
620
|
-
expect(result?.ok).toBe(true);
|
|
621
|
-
expect(result?.text).toContain("NoDesc");
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
it("slices SearXNG results to limit", async () => {
|
|
625
|
-
const mockFetch = vi.fn().mockResolvedValueOnce(
|
|
626
|
-
mockResponse({
|
|
627
|
-
ok: true,
|
|
628
|
-
json: {
|
|
629
|
-
results: Array.from({ length: 20 }, (_, i) => ({
|
|
630
|
-
title: `R${i}`,
|
|
631
|
-
url: `https://r${i}.com`,
|
|
632
|
-
content: `c${i}`,
|
|
633
|
-
})),
|
|
634
|
-
},
|
|
635
|
-
}),
|
|
636
|
-
);
|
|
637
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
638
|
-
|
|
639
|
-
const result = await handleSharedAction(
|
|
640
|
-
{ action: "web_search", query: "many", limit: 3 },
|
|
641
|
-
123,
|
|
642
|
-
);
|
|
643
|
-
// Should only contain 3 results (numbered 1-3)
|
|
644
|
-
expect(result?.text).toContain("1. R0");
|
|
645
|
-
expect(result?.text).toContain("3. R2");
|
|
646
|
-
expect(result?.text).not.toContain("4. R3");
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
|
|
650
276
|
// ════════════════════════════════════════════════════════════════════════
|
|
651
277
|
// fetch_url
|
|
652
278
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -2082,14 +1708,13 @@ describe("gateway shared actions", () => {
|
|
|
2082
1708
|
});
|
|
2083
1709
|
});
|
|
2084
1710
|
|
|
2085
|
-
// ── Additional branch coverage for fetch_url
|
|
1711
|
+
// ── Additional branch coverage for fetch_url ──────────────────────────────
|
|
2086
1712
|
|
|
2087
1713
|
describe("gateway-actions — additional branch coverage", () => {
|
|
2088
1714
|
let originalFetch: typeof globalThis.fetch;
|
|
2089
1715
|
|
|
2090
1716
|
beforeEach(() => {
|
|
2091
1717
|
originalFetch = globalThis.fetch;
|
|
2092
|
-
delete process.env.TALON_BRAVE_API_KEY;
|
|
2093
1718
|
});
|
|
2094
1719
|
|
|
2095
1720
|
afterEach(() => {
|
|
@@ -2144,51 +1769,4 @@ describe("gateway-actions — additional branch coverage", () => {
|
|
|
2144
1769
|
// Should succeed (downloaded as bin), covering ct ?? "" right side and ct.split("/")[1] ?? "file" right side
|
|
2145
1770
|
expect(result?.ok).toBe(true);
|
|
2146
1771
|
});
|
|
2147
|
-
|
|
2148
|
-
it("handles search result with missing content field (line 113 r.content ?? '' branch)", async () => {
|
|
2149
|
-
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
2150
|
-
ok: true,
|
|
2151
|
-
status: 200,
|
|
2152
|
-
headers: new Headers(),
|
|
2153
|
-
json: async () => ({
|
|
2154
|
-
results: [
|
|
2155
|
-
{ title: "Result", url: "https://example.com", content: undefined },
|
|
2156
|
-
],
|
|
2157
|
-
}),
|
|
2158
|
-
text: async () => "",
|
|
2159
|
-
arrayBuffer: async () => new ArrayBuffer(0),
|
|
2160
|
-
} as unknown as Response);
|
|
2161
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
2162
|
-
process.env.TALON_SEARXNG_URL = "http://localhost:8080";
|
|
2163
|
-
|
|
2164
|
-
const result = await handleSharedAction(
|
|
2165
|
-
{ action: "web_search", query: "test" },
|
|
2166
|
-
123,
|
|
2167
|
-
);
|
|
2168
|
-
expect(result?.ok).toBe(true);
|
|
2169
|
-
// snippet should be "" (from ?? "")
|
|
2170
|
-
expect(result?.text).toBeDefined();
|
|
2171
|
-
});
|
|
2172
|
-
|
|
2173
|
-
it("handles search response with no results array (line 113 data.results ?? [] branch)", async () => {
|
|
2174
|
-
const mockFetch = vi.fn().mockResolvedValueOnce({
|
|
2175
|
-
ok: true,
|
|
2176
|
-
status: 200,
|
|
2177
|
-
headers: new Headers(),
|
|
2178
|
-
json: async () => ({
|
|
2179
|
-
/* no results property */
|
|
2180
|
-
}),
|
|
2181
|
-
text: async () => "",
|
|
2182
|
-
arrayBuffer: async () => new ArrayBuffer(0),
|
|
2183
|
-
} as unknown as Response);
|
|
2184
|
-
vi.stubGlobal("fetch", mockFetch);
|
|
2185
|
-
process.env.TALON_SEARXNG_URL = "http://localhost:8080";
|
|
2186
|
-
|
|
2187
|
-
const result = await handleSharedAction(
|
|
2188
|
-
{ action: "web_search", query: "empty" },
|
|
2189
|
-
123,
|
|
2190
|
-
);
|
|
2191
|
-
expect(result?.ok).toBe(true);
|
|
2192
|
-
expect(result?.text).toContain("No results for");
|
|
2193
|
-
});
|
|
2194
1772
|
});
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
setSessionName,
|
|
10
10
|
} from "../../storage/sessions.js";
|
|
11
11
|
import { getChatSettings, setChatModel } from "../../storage/chat-settings.js";
|
|
12
|
-
import { getRecentHistory } from "../../storage/history.js";
|
|
13
12
|
import { resolve } from "node:path";
|
|
14
13
|
import { classify } from "../../core/errors.js";
|
|
15
14
|
import {
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
import { rebuildSystemPrompt } from "../../util/config.js";
|
|
20
19
|
import { log, logError, logWarn } from "../../util/log.js";
|
|
21
20
|
import { traceMessage } from "../../util/trace.js";
|
|
22
|
-
import {
|
|
21
|
+
import { formatFullDatetime } from "../../util/time.js";
|
|
23
22
|
|
|
24
23
|
import type { QueryParams, QueryResult } from "../../core/types.js";
|
|
25
24
|
|
|
@@ -115,66 +114,68 @@ export async function handleMessage(
|
|
|
115
114
|
"TaskOutput",
|
|
116
115
|
"TaskStop",
|
|
117
116
|
"AskUserQuestion",
|
|
117
|
+
// Always disable Claude Code built-in web tools — fetch_url is always
|
|
118
|
+
// available, and Brave Search MCP replaces WebSearch when configured.
|
|
119
|
+
"WebSearch",
|
|
120
|
+
"WebFetch",
|
|
118
121
|
],
|
|
119
122
|
...thinkingConfig,
|
|
120
123
|
mcpServers: {
|
|
121
|
-
// Register
|
|
124
|
+
// Register unified MCP tools server — one per messaging frontend.
|
|
125
|
+
// Terminal frontend relies on Claude Code built-in tools (Read, Write,
|
|
126
|
+
// Bash, etc.) and doesn't need a custom MCP tools server.
|
|
122
127
|
...(() => {
|
|
123
|
-
const
|
|
128
|
+
const allFrontends = Array.isArray(config.frontend)
|
|
124
129
|
? config.frontend
|
|
125
130
|
: [config.frontend];
|
|
131
|
+
const frontends = allFrontends.filter((f) => f !== "terminal");
|
|
126
132
|
const bridgeUrl = `http://127.0.0.1:${bridgePortFn()}`;
|
|
127
|
-
const mcpEnv = { TALON_BRIDGE_URL: bridgeUrl, TALON_CHAT_ID: chatId };
|
|
128
133
|
const servers: Record<
|
|
129
134
|
string,
|
|
130
135
|
{ command: string; args: string[]; env: Record<string, string> }
|
|
131
136
|
> = {};
|
|
132
|
-
// Resolve tsx from Talon's node_modules (cwd may be ~/.talon/workspace/ which has no node_modules)
|
|
133
137
|
// Resolve tsx from the package root (3 levels up from src/backend/claude-sdk/)
|
|
134
138
|
const tsxImport = resolve(
|
|
135
139
|
import.meta.dirname ?? ".",
|
|
136
140
|
"../../../node_modules/tsx/dist/esm/index.mjs",
|
|
137
141
|
);
|
|
142
|
+
// Unified MCP server in core/tools/
|
|
143
|
+
const mcpServerPath = resolve(
|
|
144
|
+
import.meta.dirname ?? ".",
|
|
145
|
+
"../../core/tools/mcp-server.ts",
|
|
146
|
+
);
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
: [
|
|
146
|
-
"--import",
|
|
147
|
-
tsxImport,
|
|
148
|
-
resolve(import.meta.dirname ?? ".", "tools.ts"),
|
|
149
|
-
],
|
|
150
|
-
env: mcpEnv,
|
|
148
|
+
for (const frontend of frontends) {
|
|
149
|
+
const serverName = `${frontend}-tools`;
|
|
150
|
+
const mcpEnv = {
|
|
151
|
+
TALON_BRIDGE_URL: bridgeUrl,
|
|
152
|
+
TALON_CHAT_ID: chatId,
|
|
153
|
+
TALON_FRONTEND: frontend,
|
|
151
154
|
};
|
|
152
|
-
|
|
153
|
-
if (frontends.includes("teams")) {
|
|
154
|
-
servers["teams-tools"] = {
|
|
155
|
+
servers[serverName] = {
|
|
155
156
|
command: process.platform === "win32" ? "npx" : "node",
|
|
156
157
|
args:
|
|
157
158
|
process.platform === "win32"
|
|
158
|
-
? [
|
|
159
|
-
|
|
160
|
-
resolve(
|
|
161
|
-
import.meta.dirname ?? ".",
|
|
162
|
-
"../../frontend/teams/tools.ts",
|
|
163
|
-
),
|
|
164
|
-
]
|
|
165
|
-
: [
|
|
166
|
-
"--import",
|
|
167
|
-
tsxImport,
|
|
168
|
-
resolve(
|
|
169
|
-
import.meta.dirname ?? ".",
|
|
170
|
-
"../../frontend/teams/tools.ts",
|
|
171
|
-
),
|
|
172
|
-
],
|
|
159
|
+
? ["tsx", mcpServerPath]
|
|
160
|
+
: ["--import", tsxImport, mcpServerPath],
|
|
173
161
|
env: mcpEnv,
|
|
174
162
|
};
|
|
175
163
|
}
|
|
176
164
|
return servers;
|
|
177
165
|
})(),
|
|
166
|
+
// Brave Search MCP server — provides brave_web_search and brave_local_search
|
|
167
|
+
...(config.braveApiKey
|
|
168
|
+
? {
|
|
169
|
+
"brave-search": {
|
|
170
|
+
command: resolve(
|
|
171
|
+
import.meta.dirname ?? ".",
|
|
172
|
+
"../../../node_modules/.bin/brave-search-mcp-server",
|
|
173
|
+
),
|
|
174
|
+
args: [],
|
|
175
|
+
env: { BRAVE_API_KEY: config.braveApiKey },
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
: {}),
|
|
178
179
|
...getPluginMcpServers(`http://127.0.0.1:${bridgePortFn()}`, chatId),
|
|
179
180
|
},
|
|
180
181
|
...(session.sessionId ? { resume: session.sessionId } : {}),
|
|
@@ -183,25 +184,9 @@ export async function handleMessage(
|
|
|
183
184
|
const msgIdHint = params.messageId ? ` [msg_id:${params.messageId}]` : "";
|
|
184
185
|
const nowTag = `[${formatFullDatetime()}]`;
|
|
185
186
|
|
|
186
|
-
// Session continuity: when resuming a session that has history but no active
|
|
187
|
-
// SDK session (after restart or /resume), prepend recent messages for context.
|
|
188
|
-
let continuityPrefix = "";
|
|
189
|
-
if (!session.sessionId && session.turns > 0) {
|
|
190
|
-
const recentMsgs = getRecentHistory(chatId, 10);
|
|
191
|
-
if (recentMsgs.length > 0) {
|
|
192
|
-
const contextLines = recentMsgs
|
|
193
|
-
.map((m) => {
|
|
194
|
-
const time = formatSmartTimestamp(m.timestamp);
|
|
195
|
-
return `[${time}] ${m.senderName}: ${m.text.slice(0, 300)}`;
|
|
196
|
-
})
|
|
197
|
-
.join("\n");
|
|
198
|
-
continuityPrefix = `[Session resumed — recent conversation context:\n${contextLines}]\n\n`;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
187
|
const prompt = isGroup
|
|
203
|
-
? `${
|
|
204
|
-
: `${
|
|
188
|
+
? `${nowTag} [${senderName}]${msgIdHint}: ${text}`
|
|
189
|
+
: `${nowTag}${msgIdHint} ${text}`;
|
|
205
190
|
log("agent", `[${chatId}] <- (${text.length} chars)`);
|
|
206
191
|
traceMessage(chatId, "in", text, { senderName, isGroup });
|
|
207
192
|
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
resetSession,
|
|
18
18
|
} from "../../storage/sessions.js";
|
|
19
19
|
import { getChatSettings } from "../../storage/chat-settings.js";
|
|
20
|
-
import { getRecentHistory } from "../../storage/history.js";
|
|
21
20
|
import { classify } from "../../core/errors.js";
|
|
22
21
|
import { log, logError, logWarn } from "../../util/log.js";
|
|
23
22
|
import { traceMessage } from "../../util/trace.js";
|
|
@@ -53,7 +52,7 @@ async function ensureServer(): Promise<OpencodeClient> {
|
|
|
53
52
|
|
|
54
53
|
// Register our MCP tools server with OpenCode
|
|
55
54
|
try {
|
|
56
|
-
const toolsPath = new URL("
|
|
55
|
+
const toolsPath = new URL("../../core/tools/mcp-server.ts", import.meta.url)
|
|
57
56
|
.pathname;
|
|
58
57
|
await client.mcp.add({
|
|
59
58
|
body: {
|
|
@@ -63,6 +62,7 @@ async function ensureServer(): Promise<OpencodeClient> {
|
|
|
63
62
|
command: ["node", "--import", "tsx", toolsPath],
|
|
64
63
|
environment: {
|
|
65
64
|
TALON_BRIDGE_URL: `http://127.0.0.1:${gatewayPortFn()}`,
|
|
65
|
+
TALON_FRONTEND: "telegram",
|
|
66
66
|
},
|
|
67
67
|
},
|
|
68
68
|
},
|
|
@@ -149,24 +149,9 @@ export async function handleMessage(
|
|
|
149
149
|
|
|
150
150
|
// Build prompt with group context
|
|
151
151
|
const msgIdHint = params.messageId ? ` [msg_id:${params.messageId}]` : "";
|
|
152
|
-
let continuityPrefix = "";
|
|
153
|
-
const session = getSession(chatId);
|
|
154
|
-
if (!session.sessionId && session.turns > 0) {
|
|
155
|
-
const recent = getRecentHistory(chatId, 3);
|
|
156
|
-
if (recent.length > 0) {
|
|
157
|
-
const ctx = recent
|
|
158
|
-
.map(
|
|
159
|
-
(m) =>
|
|
160
|
-
`[${new Date(m.timestamp).toISOString().slice(11, 16)}] ${m.senderName}: ${m.text.slice(0, 300)}`,
|
|
161
|
-
)
|
|
162
|
-
.join("\n");
|
|
163
|
-
continuityPrefix = `[Session resumed — recent context:\n${ctx}]\n\n`;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
152
|
const prompt = isGroup
|
|
168
|
-
?
|
|
169
|
-
: `${
|
|
153
|
+
? `[${senderName}]${msgIdHint}: ${text}`
|
|
154
|
+
: `${text}${msgIdHint}`;
|
|
170
155
|
|
|
171
156
|
log("agent", `[${chatId}] <- (${text.length} chars)`);
|
|
172
157
|
traceMessage(chatId, "in", text, { senderName, isGroup });
|
|
@@ -211,7 +196,7 @@ export async function handleMessage(
|
|
|
211
196
|
model: activeModel,
|
|
212
197
|
});
|
|
213
198
|
|
|
214
|
-
if (
|
|
199
|
+
if (getSession(chatId).turns === 0 && text) {
|
|
215
200
|
const cleanText = text
|
|
216
201
|
.replace(/^\[.*?\]\s*/g, "")
|
|
217
202
|
.replace(/\[msg_id:\d+\]\s*/g, "")
|