talon-agent 1.2.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 +7 -6
- package/prompts/dream.md +6 -2
- package/prompts/mempalace.md +57 -0
- package/src/__tests__/compose-tools.test.ts +216 -0
- package/src/__tests__/cron-store-extended.test.ts +1 -1
- package/src/__tests__/dream.test.ts +118 -1
- package/src/__tests__/fuzz.test.ts +1 -3
- package/src/__tests__/gateway-actions.test.ts +1 -423
- package/src/__tests__/gateway-retry.test.ts +0 -4
- package/src/__tests__/handlers.test.ts +0 -4
- package/src/__tests__/heartbeat.test.ts +3 -0
- package/src/__tests__/mempalace-plugin.test.ts +295 -0
- package/src/__tests__/plugin.test.ts +169 -0
- package/src/__tests__/storage-save-errors.test.ts +1 -1
- package/src/__tests__/time.test.ts +1 -1
- package/src/__tests__/watchdog.test.ts +1 -3
- package/src/__tests__/workspace.test.ts +0 -1
- package/src/backend/claude-sdk/index.ts +39 -54
- package/src/backend/opencode/index.ts +5 -20
- package/src/bootstrap.ts +140 -11
- package/src/core/dream.ts +40 -6
- package/src/core/gateway-actions.ts +0 -87
- package/src/core/plugin.ts +103 -16
- 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/frontend/telegram/handlers.ts +5 -17
- package/src/plugins/github/index.ts +106 -0
- package/src/plugins/mempalace/index.ts +147 -0
- package/src/plugins/playwright/index.ts +82 -0
- package/src/storage/sessions.ts +0 -10
- package/src/util/config.ts +31 -1
- package/src/util/log.ts +4 -1
- package/src/util/paths.ts +9 -0
- package/src/backend/claude-sdk/tools.ts +0 -651
- package/src/frontend/teams/tools.ts +0 -175
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sticker tools — browse, download, save, create, and manage sticker packs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { ToolDefinition } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export const stickerTools: ToolDefinition[] = [
|
|
9
|
+
{
|
|
10
|
+
name: "get_sticker_pack",
|
|
11
|
+
description:
|
|
12
|
+
"Get all stickers in a sticker pack by its name. Returns emoji + file_id for each sticker so you can send them. Use when you see a sticker set name in chat history.",
|
|
13
|
+
schema: {
|
|
14
|
+
set_name: z
|
|
15
|
+
.string()
|
|
16
|
+
.describe(
|
|
17
|
+
"Sticker set name (e.g. 'AnimatedEmojis' or from sticker metadata)",
|
|
18
|
+
),
|
|
19
|
+
},
|
|
20
|
+
execute: (params, bridge) => bridge("get_sticker_pack", params),
|
|
21
|
+
frontends: ["telegram"],
|
|
22
|
+
tag: "stickers",
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
name: "download_sticker",
|
|
27
|
+
description:
|
|
28
|
+
"Download a sticker image to workspace so you can view its contents. Returns the file path.",
|
|
29
|
+
schema: {
|
|
30
|
+
file_id: z
|
|
31
|
+
.string()
|
|
32
|
+
.describe("Sticker file_id from chat history or sticker pack listing"),
|
|
33
|
+
},
|
|
34
|
+
execute: (params, bridge) => bridge("download_sticker", params),
|
|
35
|
+
frontends: ["telegram"],
|
|
36
|
+
tag: "stickers",
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
name: "save_sticker_pack",
|
|
41
|
+
description:
|
|
42
|
+
"Save a sticker pack's file_ids to workspace for quick reuse. Once saved, you can read the JSON file to find stickers by emoji and send them instantly.",
|
|
43
|
+
schema: {
|
|
44
|
+
set_name: z.string().describe("Sticker set name"),
|
|
45
|
+
},
|
|
46
|
+
execute: (params, bridge) => bridge("save_sticker_pack", params),
|
|
47
|
+
frontends: ["telegram"],
|
|
48
|
+
tag: "stickers",
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
{
|
|
52
|
+
name: "create_sticker_set",
|
|
53
|
+
description: `Create a new sticker pack owned by a user. The bot will be the creator.
|
|
54
|
+
Sticker images must be PNG/WEBP, max 512x512px for static stickers.
|
|
55
|
+
The set name will automatically get "_by_<botname>" appended if needed.
|
|
56
|
+
|
|
57
|
+
Example: create_sticker_set(user_id=123, name="cool_pack", title="Cool Stickers", file_path="/path/to/sticker.png", emoji_list=["😎"])`,
|
|
58
|
+
schema: {
|
|
59
|
+
user_id: z.number().describe("Telegram user ID who will own the pack"),
|
|
60
|
+
name: z
|
|
61
|
+
.string()
|
|
62
|
+
.describe(
|
|
63
|
+
"Short name for the pack (a-z, 0-9, underscores). Will get _by_<botname> appended.",
|
|
64
|
+
),
|
|
65
|
+
title: z.string().describe("Display title for the pack (1-64 chars)"),
|
|
66
|
+
file_path: z
|
|
67
|
+
.string()
|
|
68
|
+
.describe("Path to the sticker image file (PNG/WEBP, 512x512 max)"),
|
|
69
|
+
emoji_list: z
|
|
70
|
+
.array(z.string())
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Emojis for this sticker (default: ['🎨'])"),
|
|
73
|
+
format: z
|
|
74
|
+
.enum(["static", "animated", "video"])
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Sticker format (default: static)"),
|
|
77
|
+
},
|
|
78
|
+
execute: (params, bridge) => bridge("create_sticker_set", params),
|
|
79
|
+
frontends: ["telegram"],
|
|
80
|
+
tag: "stickers",
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
name: "add_sticker_to_set",
|
|
85
|
+
description:
|
|
86
|
+
"Add a new sticker to an existing sticker pack created by the bot.",
|
|
87
|
+
schema: {
|
|
88
|
+
user_id: z.number().describe("Telegram user ID who owns the pack"),
|
|
89
|
+
name: z.string().describe("Sticker set name (including _by_<botname>)"),
|
|
90
|
+
file_path: z.string().describe("Path to the sticker image file"),
|
|
91
|
+
emoji_list: z
|
|
92
|
+
.array(z.string())
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("Emojis for this sticker (default: ['🎨'])"),
|
|
95
|
+
format: z
|
|
96
|
+
.enum(["static", "animated", "video"])
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Sticker format (default: static)"),
|
|
99
|
+
},
|
|
100
|
+
execute: (params, bridge) => bridge("add_sticker_to_set", params),
|
|
101
|
+
frontends: ["telegram"],
|
|
102
|
+
tag: "stickers",
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
{
|
|
106
|
+
name: "delete_sticker_from_set",
|
|
107
|
+
description: "Remove a specific sticker from a pack by its file_id.",
|
|
108
|
+
schema: {
|
|
109
|
+
sticker_file_id: z
|
|
110
|
+
.string()
|
|
111
|
+
.describe(
|
|
112
|
+
"file_id of the sticker to remove (get from get_sticker_pack)",
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
execute: (params, bridge) => bridge("delete_sticker_from_set", params),
|
|
116
|
+
frontends: ["telegram"],
|
|
117
|
+
tag: "stickers",
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
name: "set_sticker_set_title",
|
|
122
|
+
description: "Change the title of a sticker pack created by the bot.",
|
|
123
|
+
schema: {
|
|
124
|
+
name: z.string().describe("Sticker set name"),
|
|
125
|
+
title: z.string().describe("New title (1-64 chars)"),
|
|
126
|
+
},
|
|
127
|
+
execute: (params, bridge) => bridge("set_sticker_set_title", params),
|
|
128
|
+
frontends: ["telegram"],
|
|
129
|
+
tag: "stickers",
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
{
|
|
133
|
+
name: "delete_sticker_set",
|
|
134
|
+
description:
|
|
135
|
+
"Permanently delete an entire sticker pack created by the bot.",
|
|
136
|
+
schema: {
|
|
137
|
+
name: z.string().describe("Sticker set name to delete"),
|
|
138
|
+
},
|
|
139
|
+
execute: (params, bridge) => bridge("delete_sticker_set", params),
|
|
140
|
+
frontends: ["telegram"],
|
|
141
|
+
tag: "stickers",
|
|
142
|
+
},
|
|
143
|
+
];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the modular tool system.
|
|
3
|
+
*
|
|
4
|
+
* Tool definitions are pure data + execute logic — no MCP imports,
|
|
5
|
+
* no bridge coupling. The MCP server consumes these via composeTools().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ZodRawShape } from "zod";
|
|
9
|
+
|
|
10
|
+
/** Which frontends a tool is available on. "all" = every frontend. */
|
|
11
|
+
export type ToolFrontend = "telegram" | "teams" | "terminal" | "all";
|
|
12
|
+
|
|
13
|
+
/** Domain tags for runtime filtering and grouping. */
|
|
14
|
+
export type ToolTag =
|
|
15
|
+
| "messaging"
|
|
16
|
+
| "chat"
|
|
17
|
+
| "history"
|
|
18
|
+
| "members"
|
|
19
|
+
| "media"
|
|
20
|
+
| "stickers"
|
|
21
|
+
| "scheduling"
|
|
22
|
+
| "web";
|
|
23
|
+
|
|
24
|
+
/** The bridge caller signature — injected into execute(). */
|
|
25
|
+
export type BridgeFunction = (
|
|
26
|
+
action: string,
|
|
27
|
+
params: Record<string, unknown>,
|
|
28
|
+
) => Promise<unknown>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A self-contained tool definition.
|
|
32
|
+
*
|
|
33
|
+
* Contains everything needed to register it with an MCP server
|
|
34
|
+
* AND to know which bridge action it maps to.
|
|
35
|
+
*/
|
|
36
|
+
export interface ToolDefinition {
|
|
37
|
+
/** MCP tool name (e.g. "send", "react", "fetch_url"). */
|
|
38
|
+
readonly name: string;
|
|
39
|
+
|
|
40
|
+
/** Human-readable description shown to the model. */
|
|
41
|
+
readonly description: string;
|
|
42
|
+
|
|
43
|
+
/** Zod schema shape for the tool's input parameters. */
|
|
44
|
+
readonly schema: ZodRawShape;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Execute the tool. Receives validated params and a bridge caller.
|
|
48
|
+
* Returns the raw bridge result (wrapped by the MCP layer).
|
|
49
|
+
*/
|
|
50
|
+
readonly execute: (
|
|
51
|
+
params: Record<string, unknown>,
|
|
52
|
+
bridge: BridgeFunction,
|
|
53
|
+
) => Promise<unknown>;
|
|
54
|
+
|
|
55
|
+
/** Which frontends this tool appears on. Omit for all frontends. */
|
|
56
|
+
readonly frontends?: readonly ToolFrontend[];
|
|
57
|
+
|
|
58
|
+
/** Grouping tag. */
|
|
59
|
+
readonly tag: ToolTag;
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web tools — URL fetching.
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic, available on all frontends.
|
|
5
|
+
* Web search is handled by the Brave Search MCP server (registered in
|
|
6
|
+
* src/backend/claude-sdk/index.ts) when configured. URL fetching is provided here via
|
|
7
|
+
* the `fetch_url` tool, so Claude Code's built-in WebSearch / WebFetch are
|
|
8
|
+
* disabled in favor of these project-specific replacements.
|
|
9
|
+
* These can be excluded via composeTools({ excludeTags: ["web"] }).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import type { ToolDefinition } from "./types.js";
|
|
14
|
+
|
|
15
|
+
export const webTools: ToolDefinition[] = [
|
|
16
|
+
{
|
|
17
|
+
name: "fetch_url",
|
|
18
|
+
description:
|
|
19
|
+
"Fetch a URL — web pages return text content, image URLs are downloaded to workspace. Use to read articles, download images, or fetch any URL.",
|
|
20
|
+
schema: {
|
|
21
|
+
url: z.string().describe("The URL to fetch"),
|
|
22
|
+
},
|
|
23
|
+
execute: (params, bridge) => bridge("fetch_url", params),
|
|
24
|
+
tag: "web",
|
|
25
|
+
},
|
|
26
|
+
];
|
|
@@ -278,7 +278,10 @@ export function createTelegramActionHandler(
|
|
|
278
278
|
case "send_voice":
|
|
279
279
|
case "send_audio": {
|
|
280
280
|
const filePath = String(body.file_path ?? "");
|
|
281
|
-
const caption = body.caption
|
|
281
|
+
const caption = body.caption
|
|
282
|
+
? markdownToTelegramHtml(String(body.caption))
|
|
283
|
+
: undefined;
|
|
284
|
+
const captionParseMode = caption ? ("HTML" as const) : undefined;
|
|
282
285
|
gateway.incrementMessages(chatId);
|
|
283
286
|
if (action === "send_file") {
|
|
284
287
|
const stat = statSync(filePath);
|
|
@@ -294,6 +297,7 @@ export function createTelegramActionHandler(
|
|
|
294
297
|
sent = await withRetry(() =>
|
|
295
298
|
bot.api.sendDocument(chatId, file, {
|
|
296
299
|
caption,
|
|
300
|
+
parse_mode: captionParseMode,
|
|
297
301
|
reply_parameters: rp,
|
|
298
302
|
}),
|
|
299
303
|
);
|
|
@@ -302,6 +306,7 @@ export function createTelegramActionHandler(
|
|
|
302
306
|
sent = await withRetry(() =>
|
|
303
307
|
bot.api.sendPhoto(chatId, file, {
|
|
304
308
|
caption,
|
|
309
|
+
parse_mode: captionParseMode,
|
|
305
310
|
reply_parameters: rp,
|
|
306
311
|
}),
|
|
307
312
|
);
|
|
@@ -310,6 +315,7 @@ export function createTelegramActionHandler(
|
|
|
310
315
|
sent = await withRetry(() =>
|
|
311
316
|
bot.api.sendVideo(chatId, file, {
|
|
312
317
|
caption,
|
|
318
|
+
parse_mode: captionParseMode,
|
|
313
319
|
reply_parameters: rp,
|
|
314
320
|
}),
|
|
315
321
|
);
|
|
@@ -318,6 +324,7 @@ export function createTelegramActionHandler(
|
|
|
318
324
|
sent = await withRetry(() =>
|
|
319
325
|
bot.api.sendAnimation(chatId, file, {
|
|
320
326
|
caption,
|
|
327
|
+
parse_mode: captionParseMode,
|
|
321
328
|
reply_parameters: rp,
|
|
322
329
|
}),
|
|
323
330
|
);
|
|
@@ -326,6 +333,7 @@ export function createTelegramActionHandler(
|
|
|
326
333
|
sent = await withRetry(() =>
|
|
327
334
|
bot.api.sendAudio(chatId, file, {
|
|
328
335
|
caption,
|
|
336
|
+
parse_mode: captionParseMode,
|
|
329
337
|
reply_parameters: rp,
|
|
330
338
|
title: body.title as string | undefined,
|
|
331
339
|
performer: body.performer as string | undefined,
|
|
@@ -336,6 +344,7 @@ export function createTelegramActionHandler(
|
|
|
336
344
|
sent = await withRetry(() =>
|
|
337
345
|
bot.api.sendVoice(chatId, file, {
|
|
338
346
|
caption,
|
|
347
|
+
parse_mode: captionParseMode,
|
|
339
348
|
reply_parameters: rp,
|
|
340
349
|
}),
|
|
341
350
|
);
|
|
@@ -5,11 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Bot, Context } from "grammy";
|
|
7
7
|
import type { TalonConfig } from "../../util/config.js";
|
|
8
|
-
import {
|
|
9
|
-
splitMessage,
|
|
10
|
-
markdownToTelegramHtml,
|
|
11
|
-
escapeHtml,
|
|
12
|
-
} from "./formatting.js";
|
|
8
|
+
import { markdownToTelegramHtml, escapeHtml } from "./formatting.js";
|
|
13
9
|
import { execute } from "../../core/dispatcher.js";
|
|
14
10
|
import { classify, friendlyMessage } from "../../core/errors.js";
|
|
15
11
|
import {
|
|
@@ -391,7 +387,6 @@ const messageQueues = new Map<
|
|
|
391
387
|
messages: QueuedMessage[];
|
|
392
388
|
timer: ReturnType<typeof setTimeout>;
|
|
393
389
|
bot: Bot;
|
|
394
|
-
config: TalonConfig;
|
|
395
390
|
numericChatId: number;
|
|
396
391
|
queuedReactionMsgIds: number[];
|
|
397
392
|
}
|
|
@@ -446,7 +441,6 @@ function isUserRateLimited(senderId: number): boolean {
|
|
|
446
441
|
*/
|
|
447
442
|
function enqueueMessage(
|
|
448
443
|
bot: Bot,
|
|
449
|
-
config: TalonConfig,
|
|
450
444
|
chatId: string,
|
|
451
445
|
numericChatId: number,
|
|
452
446
|
msg: QueuedMessage,
|
|
@@ -474,7 +468,6 @@ function enqueueMessage(
|
|
|
474
468
|
messages: [msg],
|
|
475
469
|
timer: setTimeout(() => flushQueue(chatId), DEBOUNCE_MS),
|
|
476
470
|
bot,
|
|
477
|
-
config,
|
|
478
471
|
numericChatId,
|
|
479
472
|
queuedReactionMsgIds: [] as number[],
|
|
480
473
|
};
|
|
@@ -486,7 +479,7 @@ async function flushQueue(chatId: string): Promise<void> {
|
|
|
486
479
|
if (!entry) return;
|
|
487
480
|
messageQueues.delete(chatId);
|
|
488
481
|
|
|
489
|
-
const { messages, bot,
|
|
482
|
+
const { messages, bot, numericChatId, queuedReactionMsgIds } = entry;
|
|
490
483
|
|
|
491
484
|
// Clear hourglass reactions on queued messages now that we're processing
|
|
492
485
|
for (const msgId of queuedReactionMsgIds) {
|
|
@@ -516,7 +509,6 @@ async function flushQueue(chatId: string): Promise<void> {
|
|
|
516
509
|
try {
|
|
517
510
|
await processAndReply({
|
|
518
511
|
bot,
|
|
519
|
-
config,
|
|
520
512
|
chatId,
|
|
521
513
|
numericChatId,
|
|
522
514
|
replyToId: last.replyToId,
|
|
@@ -550,7 +542,6 @@ async function flushQueue(chatId: string): Promise<void> {
|
|
|
550
542
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
551
543
|
await processAndReply({
|
|
552
544
|
bot,
|
|
553
|
-
config,
|
|
554
545
|
chatId,
|
|
555
546
|
numericChatId,
|
|
556
547
|
replyToId: last.replyToId,
|
|
@@ -621,7 +612,6 @@ async function sendHtml(
|
|
|
621
612
|
*/
|
|
622
613
|
type ProcessAndReplyParams = {
|
|
623
614
|
bot: Bot;
|
|
624
|
-
config: TalonConfig;
|
|
625
615
|
chatId: string | number;
|
|
626
616
|
numericChatId: number;
|
|
627
617
|
replyToId: number;
|
|
@@ -692,7 +682,6 @@ function createStreamCallbacks(
|
|
|
692
682
|
async function processAndReply(params: ProcessAndReplyParams): Promise<void> {
|
|
693
683
|
const {
|
|
694
684
|
bot,
|
|
695
|
-
config,
|
|
696
685
|
chatId,
|
|
697
686
|
numericChatId,
|
|
698
687
|
replyToId,
|
|
@@ -864,7 +853,7 @@ async function handleMediaMessage(
|
|
|
864
853
|
|
|
865
854
|
const prompt = promptParts.join("\n");
|
|
866
855
|
|
|
867
|
-
enqueueMessage(bot,
|
|
856
|
+
enqueueMessage(bot, chatId, ctx.chat.id, {
|
|
868
857
|
prompt,
|
|
869
858
|
replyToId: ctx.message.message_id,
|
|
870
859
|
messageId: ctx.message.message_id,
|
|
@@ -920,7 +909,7 @@ export async function handleTextMessage(
|
|
|
920
909
|
);
|
|
921
910
|
const prompt = fwdCtx + replyCtx + replyPhotoCtx + (ctx.message.text ?? "");
|
|
922
911
|
|
|
923
|
-
enqueueMessage(bot,
|
|
912
|
+
enqueueMessage(bot, chatId, ctx.chat.id, {
|
|
924
913
|
prompt,
|
|
925
914
|
replyToId: ctx.message.message_id,
|
|
926
915
|
messageId: ctx.message.message_id,
|
|
@@ -1040,7 +1029,7 @@ export async function handleStickerMessage(
|
|
|
1040
1029
|
.filter(Boolean)
|
|
1041
1030
|
.join("\n");
|
|
1042
1031
|
|
|
1043
|
-
enqueueMessage(bot,
|
|
1032
|
+
enqueueMessage(bot, chatId, ctx.chat.id, {
|
|
1044
1033
|
prompt,
|
|
1045
1034
|
replyToId: ctx.message.message_id,
|
|
1046
1035
|
messageId: ctx.message.message_id,
|
|
@@ -1185,7 +1174,6 @@ export async function handleCallbackQuery(
|
|
|
1185
1174
|
|
|
1186
1175
|
await processAndReply({
|
|
1187
1176
|
bot,
|
|
1188
|
-
config,
|
|
1189
1177
|
chatId,
|
|
1190
1178
|
numericChatId,
|
|
1191
1179
|
replyToId,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub plugin — GitHub API access via the official GitHub MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Registers the GitHub MCP server (Docker image: ghcr.io/github/github-mcp-server),
|
|
5
|
+
* giving the agent access to repository management, issues, PRs, code search, etc.
|
|
6
|
+
*
|
|
7
|
+
* Configuration in ~/.talon/config.json:
|
|
8
|
+
* "github": {
|
|
9
|
+
* "enabled": true,
|
|
10
|
+
* "token": "ghp_..." // optional, defaults to `gh auth token` output
|
|
11
|
+
* }
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
import type { TalonPlugin } from "../../core/plugin.js";
|
|
16
|
+
import { log, logWarn } from "../../util/log.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a GitHub personal access token.
|
|
20
|
+
* Priority: explicit config > `gh auth token` CLI.
|
|
21
|
+
*/
|
|
22
|
+
function resolveToken(configToken?: string): string | undefined {
|
|
23
|
+
const trimmed = configToken?.trim();
|
|
24
|
+
if (trimmed) return trimmed;
|
|
25
|
+
try {
|
|
26
|
+
return execFileSync("gh", ["auth", "token"], {
|
|
27
|
+
timeout: 5_000,
|
|
28
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
29
|
+
})
|
|
30
|
+
.toString("utf-8")
|
|
31
|
+
.trim();
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createGitHubPlugin(config: { token?: string }): TalonPlugin {
|
|
38
|
+
const token = resolveToken(config.token);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
name: "github",
|
|
42
|
+
description: "GitHub API access via the official GitHub MCP server",
|
|
43
|
+
version: "1.0.0",
|
|
44
|
+
|
|
45
|
+
mcpServer: {
|
|
46
|
+
command: "docker",
|
|
47
|
+
args: [
|
|
48
|
+
"run",
|
|
49
|
+
"--rm",
|
|
50
|
+
"-i",
|
|
51
|
+
"-e",
|
|
52
|
+
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
53
|
+
"ghcr.io/github/github-mcp-server",
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
validateConfig() {
|
|
58
|
+
const errors: string[] = [];
|
|
59
|
+
|
|
60
|
+
if (!token) {
|
|
61
|
+
errors.push(
|
|
62
|
+
'No GitHub token found. Set "token" in github config or run `gh auth login`.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check Docker is available
|
|
67
|
+
try {
|
|
68
|
+
execFileSync("docker", ["info"], {
|
|
69
|
+
timeout: 10_000,
|
|
70
|
+
stdio: "pipe",
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
errors.push(
|
|
74
|
+
"Docker is not available or not running. The GitHub MCP server requires Docker.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return errors.length > 0 ? errors : undefined;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async init() {
|
|
82
|
+
// Verify the Docker image exists locally
|
|
83
|
+
try {
|
|
84
|
+
execFileSync(
|
|
85
|
+
"docker",
|
|
86
|
+
["image", "inspect", "ghcr.io/github/github-mcp-server"],
|
|
87
|
+
{ timeout: 10_000, stdio: "pipe" },
|
|
88
|
+
);
|
|
89
|
+
log("github", "Docker image verified");
|
|
90
|
+
} catch {
|
|
91
|
+
logWarn(
|
|
92
|
+
"github",
|
|
93
|
+
"Docker image not found locally — will pull on first use (may be slow)",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
log("github", "Ready");
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
getEnvVars() {
|
|
101
|
+
return {
|
|
102
|
+
...(token ? { GITHUB_PERSONAL_ACCESS_TOKEN: token } : {}),
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MemPalace plugin — structured long-term memory with vector search.
|
|
3
|
+
*
|
|
4
|
+
* Registers the mempalace Python MCP server, giving the agent access to
|
|
5
|
+
* semantic memory search, knowledge graph operations, and diary entries.
|
|
6
|
+
*
|
|
7
|
+
* Configuration in ~/.talon/config.json:
|
|
8
|
+
* "mempalace": {
|
|
9
|
+
* "enabled": true,
|
|
10
|
+
* "palacePath": "/path/to/palace", // optional, defaults to ~/.talon/workspace/palace/
|
|
11
|
+
* "pythonPath": "/path/to/python" // optional, defaults to mempalace venv python (bin/python on Unix, Scripts/python.exe on Windows)
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { execFile as execFileCb, execFileSync } from "node:child_process";
|
|
18
|
+
import { promisify } from "node:util";
|
|
19
|
+
import type { TalonPlugin } from "../../core/plugin.js";
|
|
20
|
+
import { log, logWarn } from "../../util/log.js";
|
|
21
|
+
import { dirs } from "../../util/paths.js";
|
|
22
|
+
|
|
23
|
+
const execFile = promisify(execFileCb);
|
|
24
|
+
|
|
25
|
+
/** Load from ~/.talon/prompts/ (user-customisable, seeded on first run) */
|
|
26
|
+
const PROMPT_PATH = resolve(dirs.prompts, "mempalace.md");
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a mempalace plugin instance with resolved paths.
|
|
30
|
+
* Uses a factory because MCP server command/args depend on runtime config.
|
|
31
|
+
*/
|
|
32
|
+
export function createMempalacePlugin(config: {
|
|
33
|
+
pythonPath: string;
|
|
34
|
+
palacePath: string;
|
|
35
|
+
}): TalonPlugin {
|
|
36
|
+
const { pythonPath, palacePath } = config;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: "mempalace",
|
|
40
|
+
description:
|
|
41
|
+
"Memory palace — structured long-term memory with vector search",
|
|
42
|
+
version: "1.0.0",
|
|
43
|
+
|
|
44
|
+
mcpServer: {
|
|
45
|
+
command: pythonPath,
|
|
46
|
+
args: ["-m", "mempalace.mcp_server", "--palace", palacePath],
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
validateConfig() {
|
|
50
|
+
const errors: string[] = [];
|
|
51
|
+
if (!existsSync(pythonPath)) {
|
|
52
|
+
errors.push(
|
|
53
|
+
`Python binary not found at ${pythonPath}. Create or select a Python environment, set "pythonPath" to that interpreter, then run: ${pythonPath} -m pip install mempalace`,
|
|
54
|
+
);
|
|
55
|
+
return errors;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Verify mempalace.mcp_server is importable (the actual module spawned by MCP)
|
|
59
|
+
try {
|
|
60
|
+
execFileSync(pythonPath, ["-c", "import mempalace.mcp_server"], {
|
|
61
|
+
timeout: 15_000,
|
|
62
|
+
stdio: "pipe",
|
|
63
|
+
});
|
|
64
|
+
} catch (err: unknown) {
|
|
65
|
+
const execErr =
|
|
66
|
+
err && typeof err === "object"
|
|
67
|
+
? (err as {
|
|
68
|
+
code?: string;
|
|
69
|
+
signal?: string;
|
|
70
|
+
killed?: boolean;
|
|
71
|
+
stderr?: string | Buffer;
|
|
72
|
+
})
|
|
73
|
+
: undefined;
|
|
74
|
+
const code = execErr?.code;
|
|
75
|
+
if (code === "ENOENT" || code === "EACCES" || code === "EPERM") {
|
|
76
|
+
errors.push(
|
|
77
|
+
`Cannot execute Python at ${pythonPath} (${code}). Check that the path is correct and the binary is executable.`,
|
|
78
|
+
);
|
|
79
|
+
} else if (code === "ETIMEDOUT" || execErr?.killed || execErr?.signal) {
|
|
80
|
+
errors.push(
|
|
81
|
+
`Python import check timed out or was killed. The interpreter at ${pythonPath} may be unresponsive.`,
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
const stderr =
|
|
85
|
+
typeof execErr?.stderr === "string"
|
|
86
|
+
? execErr.stderr.trim()
|
|
87
|
+
: Buffer.isBuffer(execErr?.stderr)
|
|
88
|
+
? execErr.stderr.toString("utf-8").trim()
|
|
89
|
+
: "";
|
|
90
|
+
errors.push(
|
|
91
|
+
`mempalace package not installed or mcp_server submodule missing. Run: ${pythonPath} -m pip install mempalace${stderr ? `. Details: ${stderr}` : ""}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return errors.length > 0 ? errors : undefined;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async init() {
|
|
100
|
+
// Ensure palace directory exists
|
|
101
|
+
if (!existsSync(palacePath)) {
|
|
102
|
+
mkdirSync(palacePath, { recursive: true });
|
|
103
|
+
log("mempalace", `Created palace directory: ${palacePath}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Quick smoke test — verify mempalace can import and access the palace path
|
|
107
|
+
try {
|
|
108
|
+
const { stdout } = await execFile(
|
|
109
|
+
pythonPath,
|
|
110
|
+
[
|
|
111
|
+
"-c",
|
|
112
|
+
`import mempalace; print(f"mempalace {mempalace.__version__}" if hasattr(mempalace, "__version__") else "mempalace ok")`,
|
|
113
|
+
],
|
|
114
|
+
{ timeout: 15_000 },
|
|
115
|
+
);
|
|
116
|
+
log("mempalace", stdout.trim() || "Module verified");
|
|
117
|
+
} catch {
|
|
118
|
+
// Non-fatal — MCP server handles lazy init
|
|
119
|
+
log(
|
|
120
|
+
"mempalace",
|
|
121
|
+
"Module import check skipped — MCP server will initialize on first use",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
log("mempalace", `Ready (palace: ${palacePath})`);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
getEnvVars() {
|
|
129
|
+
return {
|
|
130
|
+
MEMPALACE_PALACE_PATH: palacePath,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
getSystemPromptAddition() {
|
|
135
|
+
try {
|
|
136
|
+
const template = readFileSync(PROMPT_PATH, "utf-8");
|
|
137
|
+
return template.replace(/\{\{palacePath\}\}/g, palacePath);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
logWarn(
|
|
140
|
+
"mempalace",
|
|
141
|
+
`Failed to load prompt from ${PROMPT_PATH}: ${err instanceof Error ? err.message : err}`,
|
|
142
|
+
);
|
|
143
|
+
return `## MemPalace — Long-term Memory\n\nPalace location: \`${palacePath}\``;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|