tg-agent 0.1.0 → 1.1.1
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 +7 -0
- package/dist/auth.js +1 -71
- package/dist/cli.js +1 -1
- package/dist/codexAuth.js +1 -93
- package/dist/config.js +1 -59
- package/dist/customTools.js +9 -386
- package/dist/index.js +17 -954
- package/dist/mcp.js +5 -427
- package/dist/piAgentRunner.js +7 -407
- package/dist/piAiRunner.js +5 -99
- package/dist/proxy.js +1 -19
- package/dist/sessionStore.js +1 -138
- package/dist/types.js +0 -1
- package/dist/utils.js +2 -91
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
- 多 Provider / 多 Model 选择(支持 Codex / antigravity / Gemini CLI 等)
|
|
13
13
|
- OAuth 登录与注销(/login, /logout)
|
|
14
14
|
- 工具调用:内置 `fetch`,可选 MCP(HTTP/stdio)
|
|
15
|
+
- 图片/文件上传与发送(Telegram 附件、`send_photo`/`send_file` 工具)
|
|
15
16
|
- 代理支持:模型与工具请求可分别配置
|
|
16
17
|
|
|
17
18
|
## 快速开始
|
|
@@ -74,6 +75,12 @@ npm start
|
|
|
74
75
|
|
|
75
76
|
默认仅支持私聊(DM)。
|
|
76
77
|
|
|
78
|
+
### 图片与文件
|
|
79
|
+
|
|
80
|
+
- 直接发送图片或文件给机器人,会保存到 `~/.tg-agent/uploads`,并自动附加到提示词中。
|
|
81
|
+
- 图片会作为多模态输入传给模型(若模型支持图像)。
|
|
82
|
+
- Agent 可使用 `send_photo` / `send_file` 工具把本地文件发回 Telegram(路径需在工作目录或 uploads 下)。
|
|
83
|
+
|
|
77
84
|
## 环境变量
|
|
78
85
|
|
|
79
86
|
`.env.example` 已列出所有可配置项。常用说明如下:
|
package/dist/auth.js
CHANGED
|
@@ -1,71 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { readCodexOAuth } from "./codexAuth.js";
|
|
3
|
-
export function resolveApiKeyForProvider(providerRaw) {
|
|
4
|
-
const provider = providerRaw.trim().toLowerCase();
|
|
5
|
-
if (provider === "openai-codex" || provider === "codex") {
|
|
6
|
-
const codex = readCodexOAuth();
|
|
7
|
-
if (codex) {
|
|
8
|
-
return { apiKey: codex.accessToken, source: `codex:${codex.source}` };
|
|
9
|
-
}
|
|
10
|
-
throw new Error("No Codex OAuth credentials found. Run `codex login`.");
|
|
11
|
-
}
|
|
12
|
-
if (config.openaiApiKey) {
|
|
13
|
-
return { apiKey: config.openaiApiKey, source: "env:OPENAI_API_KEY" };
|
|
14
|
-
}
|
|
15
|
-
throw new Error(`No API key for provider: ${providerRaw}`);
|
|
16
|
-
}
|
|
17
|
-
function resolveProxyUrl() {
|
|
18
|
-
const candidates = [
|
|
19
|
-
{ key: "PROXY_URL", value: process.env.PROXY_URL },
|
|
20
|
-
{ key: "FETCH_PROXY_URL", value: process.env.FETCH_PROXY_URL },
|
|
21
|
-
{ key: "HTTPS_PROXY", value: process.env.HTTPS_PROXY },
|
|
22
|
-
{ key: "https_proxy", value: process.env.https_proxy },
|
|
23
|
-
{ key: "HTTP_PROXY", value: process.env.HTTP_PROXY },
|
|
24
|
-
{ key: "http_proxy", value: process.env.http_proxy },
|
|
25
|
-
{ key: "ALL_PROXY", value: process.env.ALL_PROXY },
|
|
26
|
-
{ key: "all_proxy", value: process.env.all_proxy },
|
|
27
|
-
];
|
|
28
|
-
for (const entry of candidates) {
|
|
29
|
-
const raw = entry.value?.trim();
|
|
30
|
-
if (raw) {
|
|
31
|
-
return { url: raw, source: entry.key };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
function detectProxyKind(url) {
|
|
37
|
-
const lower = url.toLowerCase();
|
|
38
|
-
if (lower.startsWith("socks5://") || lower.startsWith("socks4://") || lower.startsWith("socks://")) {
|
|
39
|
-
return "socks";
|
|
40
|
-
}
|
|
41
|
-
if (lower.startsWith("https://")) {
|
|
42
|
-
return "https";
|
|
43
|
-
}
|
|
44
|
-
return "http";
|
|
45
|
-
}
|
|
46
|
-
export function resolveProxyInfo() {
|
|
47
|
-
const resolved = resolveProxyUrl();
|
|
48
|
-
if (!resolved)
|
|
49
|
-
return null;
|
|
50
|
-
return { url: resolved.url, kind: detectProxyKind(resolved.url), source: resolved.source };
|
|
51
|
-
}
|
|
52
|
-
export function resolveFetchProxyInfo() {
|
|
53
|
-
const candidates = [
|
|
54
|
-
{ key: "FETCH_PROXY_URL", value: process.env.FETCH_PROXY_URL },
|
|
55
|
-
{ key: "HTTPS_PROXY", value: process.env.HTTPS_PROXY },
|
|
56
|
-
{ key: "https_proxy", value: process.env.https_proxy },
|
|
57
|
-
{ key: "HTTP_PROXY", value: process.env.HTTP_PROXY },
|
|
58
|
-
{ key: "http_proxy", value: process.env.http_proxy },
|
|
59
|
-
];
|
|
60
|
-
for (const entry of candidates) {
|
|
61
|
-
const raw = entry.value?.trim();
|
|
62
|
-
if (!raw)
|
|
63
|
-
continue;
|
|
64
|
-
const kind = detectProxyKind(raw);
|
|
65
|
-
if (kind === "socks") {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
return { url: raw, kind, source: entry.key };
|
|
69
|
-
}
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
1
|
+
import{config as s}from"./config.js";import{readCodexOAuth as c}from"./codexAuth.js";function l(e){const r=e.trim().toLowerCase();if(r==="openai-codex"||r==="codex"){const o=c();if(o)return{apiKey:o.accessToken,source:`codex:${o.source}`};throw new Error("No Codex OAuth credentials found. Run `codex login`.")}if(s.openaiApiKey)return{apiKey:s.openaiApiKey,source:"env:OPENAI_API_KEY"};throw new Error(`No API key for provider: ${e}`)}function u(){const e=[{key:"PROXY_URL",value:process.env.PROXY_URL},{key:"FETCH_PROXY_URL",value:process.env.FETCH_PROXY_URL},{key:"HTTPS_PROXY",value:process.env.HTTPS_PROXY},{key:"https_proxy",value:process.env.https_proxy},{key:"HTTP_PROXY",value:process.env.HTTP_PROXY},{key:"http_proxy",value:process.env.http_proxy},{key:"ALL_PROXY",value:process.env.ALL_PROXY},{key:"all_proxy",value:process.env.all_proxy}];for(const r of e){const o=r.value?.trim();if(o)return{url:o,source:r.key}}return null}function n(e){const r=e.toLowerCase();return r.startsWith("socks5://")||r.startsWith("socks4://")||r.startsWith("socks://")?"socks":r.startsWith("https://")?"https":"http"}function y(){const e=u();return e?{url:e.url,kind:n(e.url),source:e.source}:null}function a(){const e=[{key:"FETCH_PROXY_URL",value:process.env.FETCH_PROXY_URL},{key:"HTTPS_PROXY",value:process.env.HTTPS_PROXY},{key:"https_proxy",value:process.env.https_proxy},{key:"HTTP_PROXY",value:process.env.HTTP_PROXY},{key:"http_proxy",value:process.env.http_proxy}];for(const r of e){const o=r.value?.trim();if(!o)continue;const t=n(o);if(t!=="socks")return{url:o,kind:t,source:r.key}}return null}export{l as resolveApiKeyForProvider,a as resolveFetchProxyInfo,y as resolveProxyInfo};
|
package/dist/cli.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import"./index.js";
|
package/dist/codexAuth.js
CHANGED
|
@@ -1,93 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { execSync } from "node:child_process";
|
|
6
|
-
const CODEX_AUTH_FILENAME = "auth.json";
|
|
7
|
-
const CODEX_KEYCHAIN_SERVICE = "Codex Auth";
|
|
8
|
-
function resolveCodexHome() {
|
|
9
|
-
const configured = process.env.CODEX_HOME?.trim();
|
|
10
|
-
const base = configured ? configured : "~/.codex";
|
|
11
|
-
return expandHome(base);
|
|
12
|
-
}
|
|
13
|
-
function resolveCodexAuthPath() {
|
|
14
|
-
return path.join(resolveCodexHome(), CODEX_AUTH_FILENAME);
|
|
15
|
-
}
|
|
16
|
-
function expandHome(inputPath) {
|
|
17
|
-
if (!inputPath.startsWith("~")) {
|
|
18
|
-
return inputPath;
|
|
19
|
-
}
|
|
20
|
-
return path.join(os.homedir(), inputPath.slice(1));
|
|
21
|
-
}
|
|
22
|
-
function computeKeychainAccount(codexHome) {
|
|
23
|
-
const hash = createHash("sha256").update(codexHome).digest("hex");
|
|
24
|
-
return `cli|${hash.slice(0, 16)}`;
|
|
25
|
-
}
|
|
26
|
-
function readCodexFromKeychain() {
|
|
27
|
-
if (process.platform !== "darwin") {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const codexHome = resolveCodexHome();
|
|
31
|
-
const account = computeKeychainAccount(codexHome);
|
|
32
|
-
try {
|
|
33
|
-
const secret = execSync(`security find-generic-password -s "${CODEX_KEYCHAIN_SERVICE}" -a "${account}" -w`, { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
34
|
-
const parsed = JSON.parse(secret);
|
|
35
|
-
const tokens = parsed.tokens;
|
|
36
|
-
const accessToken = tokens?.access_token;
|
|
37
|
-
const refreshToken = tokens?.refresh_token;
|
|
38
|
-
if (typeof accessToken !== "string" || !accessToken)
|
|
39
|
-
return null;
|
|
40
|
-
if (typeof refreshToken !== "string" || !refreshToken)
|
|
41
|
-
return null;
|
|
42
|
-
const lastRefreshRaw = parsed.last_refresh;
|
|
43
|
-
const lastRefresh = typeof lastRefreshRaw === "string" || typeof lastRefreshRaw === "number"
|
|
44
|
-
? new Date(lastRefreshRaw).getTime()
|
|
45
|
-
: Date.now();
|
|
46
|
-
const expiresAt = Number.isFinite(lastRefresh)
|
|
47
|
-
? lastRefresh + 60 * 60 * 1000
|
|
48
|
-
: Date.now() + 60 * 60 * 1000;
|
|
49
|
-
return {
|
|
50
|
-
accessToken,
|
|
51
|
-
refreshToken,
|
|
52
|
-
expiresAt,
|
|
53
|
-
source: "keychain",
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function readCodexFromFile() {
|
|
61
|
-
const authPath = resolveCodexAuthPath();
|
|
62
|
-
try {
|
|
63
|
-
const raw = fs.readFileSync(authPath, "utf8");
|
|
64
|
-
const parsed = JSON.parse(raw);
|
|
65
|
-
const tokens = parsed.tokens;
|
|
66
|
-
const accessToken = tokens?.access_token;
|
|
67
|
-
const refreshToken = tokens?.refresh_token;
|
|
68
|
-
if (typeof accessToken !== "string" || !accessToken)
|
|
69
|
-
return null;
|
|
70
|
-
if (typeof refreshToken !== "string" || !refreshToken)
|
|
71
|
-
return null;
|
|
72
|
-
let expiresAt = Date.now() + 60 * 60 * 1000;
|
|
73
|
-
try {
|
|
74
|
-
const stat = fs.statSync(authPath);
|
|
75
|
-
expiresAt = stat.mtimeMs + 60 * 60 * 1000;
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// ignore
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
accessToken,
|
|
82
|
-
refreshToken,
|
|
83
|
-
expiresAt,
|
|
84
|
-
source: "file",
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
export function readCodexOAuth() {
|
|
92
|
-
return readCodexFromKeychain() ?? readCodexFromFile();
|
|
93
|
-
}
|
|
1
|
+
import{createHash as l}from"node:crypto";import u from"node:fs";import m from"node:os";import f from"node:path";import{execSync as d}from"node:child_process";const y="auth.json",x="Codex Auth";function p(){const e=process.env.CODEX_HOME?.trim();return C(e||"~/.codex")}function k(){return f.join(p(),y)}function C(e){return e.startsWith("~")?f.join(m.homedir(),e.slice(1)):e}function g(e){return`cli|${l("sha256").update(e).digest("hex").slice(0,16)}`}function _(){if(process.platform!=="darwin")return null;const e=p(),o=g(e);try{const i=d(`security find-generic-password -s "${x}" -a "${o}" -w`,{encoding:"utf8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim(),s=JSON.parse(i),n=s.tokens,t=n?.access_token,r=n?.refresh_token;if(typeof t!="string"||!t||typeof r!="string"||!r)return null;const c=s.last_refresh,a=typeof c=="string"||typeof c=="number"?new Date(c).getTime():Date.now(),h=Number.isFinite(a)?a+3600*1e3:Date.now()+3600*1e3;return{accessToken:t,refreshToken:r,expiresAt:h,source:"keychain"}}catch{return null}}function w(){const e=k();try{const o=u.readFileSync(e,"utf8"),s=JSON.parse(o).tokens,n=s?.access_token,t=s?.refresh_token;if(typeof n!="string"||!n||typeof t!="string"||!t)return null;let r=Date.now()+3600*1e3;try{r=u.statSync(e).mtimeMs+3600*1e3}catch{}return{accessToken:n,refreshToken:t,expiresAt:r,source:"file"}}catch{return null}}function F(){return _()??w()}export{F as readCodexOAuth};
|
package/dist/config.js
CHANGED
|
@@ -1,59 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import dotenv from "dotenv";
|
|
3
|
-
import { expandHome } from "./utils.js";
|
|
4
|
-
dotenv.config();
|
|
5
|
-
function parseNumber(value, fallback) {
|
|
6
|
-
if (!value) {
|
|
7
|
-
return fallback;
|
|
8
|
-
}
|
|
9
|
-
const parsed = Number.parseInt(value, 10);
|
|
10
|
-
return Number.isNaN(parsed) ? fallback : parsed;
|
|
11
|
-
}
|
|
12
|
-
function parseStringSet(value) {
|
|
13
|
-
if (!value) {
|
|
14
|
-
return new Set();
|
|
15
|
-
}
|
|
16
|
-
const items = value
|
|
17
|
-
.split(",")
|
|
18
|
-
.map((item) => item.trim())
|
|
19
|
-
.filter((item) => item.length > 0);
|
|
20
|
-
return new Set(items);
|
|
21
|
-
}
|
|
22
|
-
const sessionDirRaw = process.env.SESSION_DIR ?? "~/.codex/tg-sessions";
|
|
23
|
-
const agentDirRaw = process.env.AGENT_DIR ?? "~/.tg-agent";
|
|
24
|
-
const workspaceDirRaw = process.env.WORKSPACE_DIR ?? process.cwd();
|
|
25
|
-
const modelProviderRaw = process.env.LLM_PROVIDER ?? "openai-codex";
|
|
26
|
-
const modelRefRaw = process.env.LLM_MODEL ?? process.env.CODEX_MODEL ?? "gpt-5.2";
|
|
27
|
-
const telegramParseModeRaw = process.env.TELEGRAM_PARSE_MODE ?? "";
|
|
28
|
-
const telegramAllowedUsersRaw = process.env.TELEGRAM_ALLOWED_USER_IDS ?? "";
|
|
29
|
-
export const config = {
|
|
30
|
-
telegramToken: process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "",
|
|
31
|
-
openaiApiKey: process.env.OPENAI_API_KEY?.trim() ?? "",
|
|
32
|
-
modelProvider: modelProviderRaw.trim(),
|
|
33
|
-
modelRef: modelRefRaw.trim(),
|
|
34
|
-
telegramParseMode: telegramParseModeRaw.trim(),
|
|
35
|
-
telegramAllowedUsers: parseStringSet(telegramAllowedUsersRaw),
|
|
36
|
-
sessionDir: expandHome(sessionDirRaw),
|
|
37
|
-
agentDir: expandHome(agentDirRaw),
|
|
38
|
-
workspaceDir: path.resolve(expandHome(workspaceDirRaw)),
|
|
39
|
-
maxSessions: parseNumber(process.env.MAX_SESSIONS, 3),
|
|
40
|
-
maxConcurrent: parseNumber(process.env.MAX_CONCURRENT, 3),
|
|
41
|
-
sessionTtlMs: parseNumber(process.env.SESSION_TTL_MIN, 60) * 60 * 1000,
|
|
42
|
-
maxHistoryMessages: parseNumber(process.env.MAX_HISTORY_MESSAGES, 40),
|
|
43
|
-
maxOutputTokens: parseNumber(process.env.MAX_OUTPUT_TOKENS, 0),
|
|
44
|
-
fetchMaxBytes: parseNumber(process.env.FETCH_MAX_BYTES, 200_000),
|
|
45
|
-
fetchTimeoutMs: parseNumber(process.env.FETCH_TIMEOUT_MS, 60_000),
|
|
46
|
-
modelTimeoutMs: parseNumber(process.env.MODEL_TIMEOUT_MS, 60_000),
|
|
47
|
-
modelTimeoutStreamingMs: parseNumber(process.env.MODEL_TIMEOUT_STREAM_MS, 300_000),
|
|
48
|
-
systemPrompt: process.env.SYSTEM_PROMPT ??
|
|
49
|
-
"You are running in user's personal computer or VPS, using telegram bot to interact with user. Be concise and practical.",
|
|
50
|
-
};
|
|
51
|
-
export function assertConfig() {
|
|
52
|
-
const missing = [];
|
|
53
|
-
if (!config.telegramToken) {
|
|
54
|
-
missing.push("TELEGRAM_BOT_TOKEN");
|
|
55
|
-
}
|
|
56
|
-
if (missing.length > 0) {
|
|
57
|
-
throw new Error(`Missing env vars: ${missing.join(", ")}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
import t from"node:path";import i from"dotenv";import{expandHome as n}from"./utils.js";i.config();function s(e,o){if(!e)return o;const r=Number.parseInt(e,10);return Number.isNaN(r)?o:r}function c(e){if(!e)return new Set;const o=e.split(",").map(r=>r.trim()).filter(r=>r.length>0);return new Set(o)}const p=process.env.SESSION_DIR??"~/.codex/tg-sessions",m=process.env.AGENT_DIR??"~/.tg-agent",a=process.env.WORKSPACE_DIR??process.cwd(),E=process.env.LLM_PROVIDER??"openai-codex",_=process.env.LLM_MODEL??process.env.CODEX_MODEL??"gpt-5.2",M=process.env.TELEGRAM_PARSE_MODE??"",T=process.env.TELEGRAM_ALLOWED_USER_IDS??"",S={telegramToken:process.env.TELEGRAM_BOT_TOKEN?.trim()??"",openaiApiKey:process.env.OPENAI_API_KEY?.trim()??"",modelProvider:E.trim(),modelRef:_.trim(),telegramParseMode:M.trim(),telegramAllowedUsers:c(T),sessionDir:n(p),agentDir:n(m),workspaceDir:t.resolve(n(a)),maxSessions:s(process.env.MAX_SESSIONS,3),maxConcurrent:s(process.env.MAX_CONCURRENT,3),sessionTtlMs:s(process.env.SESSION_TTL_MIN,60)*60*1e3,maxHistoryMessages:s(process.env.MAX_HISTORY_MESSAGES,40),maxOutputTokens:s(process.env.MAX_OUTPUT_TOKENS,0),fetchMaxBytes:s(process.env.FETCH_MAX_BYTES,2e5),fetchTimeoutMs:s(process.env.FETCH_TIMEOUT_MS,6e4),modelTimeoutMs:s(process.env.MODEL_TIMEOUT_MS,6e4),modelTimeoutStreamingMs:s(process.env.MODEL_TIMEOUT_STREAM_MS,3e5),systemPrompt:process.env.SYSTEM_PROMPT??"You are running in user's personal computer or VPS, using telegram bot to interact with user. Be concise and practical."};function v(){const e=[];if(S.telegramToken||e.push("TELEGRAM_BOT_TOKEN"),e.length>0)throw new Error(`Missing env vars: ${e.join(", ")}`)}export{v as assertConfig,S as config};
|
package/dist/customTools.js
CHANGED
|
@@ -1,386 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
function normalizeHeaders(headers) {
|
|
12
|
-
if (!headers)
|
|
13
|
-
return {};
|
|
14
|
-
const normalized = {};
|
|
15
|
-
if (Array.isArray(headers)) {
|
|
16
|
-
for (const entry of headers) {
|
|
17
|
-
if (!entry?.name)
|
|
18
|
-
continue;
|
|
19
|
-
normalized[entry.name] = entry.value ?? "";
|
|
20
|
-
}
|
|
21
|
-
return normalized;
|
|
22
|
-
}
|
|
23
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
24
|
-
if (!key)
|
|
25
|
-
continue;
|
|
26
|
-
normalized[key] = value;
|
|
27
|
-
}
|
|
28
|
-
return normalized;
|
|
29
|
-
}
|
|
30
|
-
function resolveTimeoutMs(input) {
|
|
31
|
-
const fallback = config.fetchTimeoutMs > 0 ? config.fetchTimeoutMs : DEFAULT_TIMEOUT_MS;
|
|
32
|
-
if (!input)
|
|
33
|
-
return clampNumber(fallback, 1000, 120_000);
|
|
34
|
-
return clampNumber(input, 1000, 120_000);
|
|
35
|
-
}
|
|
36
|
-
function resolveMaxBytes(input) {
|
|
37
|
-
const fallback = config.fetchMaxBytes > 0 ? config.fetchMaxBytes : DEFAULT_MAX_BYTES;
|
|
38
|
-
if (!input)
|
|
39
|
-
return clampNumber(fallback, 1024, 5_000_000);
|
|
40
|
-
return clampNumber(input, 1024, 5_000_000);
|
|
41
|
-
}
|
|
42
|
-
function isHttpUrl(raw) {
|
|
43
|
-
try {
|
|
44
|
-
const url = new URL(raw);
|
|
45
|
-
return url.protocol === "http:" || url.protocol === "https:";
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function concatChunks(chunks, totalBytes) {
|
|
52
|
-
const buffer = new Uint8Array(totalBytes);
|
|
53
|
-
let offset = 0;
|
|
54
|
-
for (const chunk of chunks) {
|
|
55
|
-
buffer.set(chunk, offset);
|
|
56
|
-
offset += chunk.byteLength;
|
|
57
|
-
}
|
|
58
|
-
return buffer;
|
|
59
|
-
}
|
|
60
|
-
async function readBodyLimited(response, maxBytes, signal) {
|
|
61
|
-
const reader = response.body?.getReader?.();
|
|
62
|
-
if (!reader) {
|
|
63
|
-
const text = (await response.text?.()) ?? "";
|
|
64
|
-
const bytes = Math.min(text.length, maxBytes);
|
|
65
|
-
const truncated = text.length > maxBytes;
|
|
66
|
-
return { text: text.slice(0, maxBytes), bytes, truncated };
|
|
67
|
-
}
|
|
68
|
-
const decoder = new TextDecoder("utf-8");
|
|
69
|
-
const chunks = [];
|
|
70
|
-
let bytes = 0;
|
|
71
|
-
let truncated = false;
|
|
72
|
-
while (true) {
|
|
73
|
-
if (signal?.aborted) {
|
|
74
|
-
try {
|
|
75
|
-
await reader.cancel?.();
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// ignore
|
|
79
|
-
}
|
|
80
|
-
throw new Error("Fetch aborted");
|
|
81
|
-
}
|
|
82
|
-
const { done, value } = await reader.read();
|
|
83
|
-
if (done)
|
|
84
|
-
break;
|
|
85
|
-
if (!value)
|
|
86
|
-
continue;
|
|
87
|
-
const nextBytes = bytes + value.byteLength;
|
|
88
|
-
if (nextBytes > maxBytes) {
|
|
89
|
-
const sliceSize = Math.max(0, maxBytes - bytes);
|
|
90
|
-
if (sliceSize > 0) {
|
|
91
|
-
chunks.push(value.slice(0, sliceSize));
|
|
92
|
-
bytes += sliceSize;
|
|
93
|
-
}
|
|
94
|
-
truncated = true;
|
|
95
|
-
try {
|
|
96
|
-
await reader.cancel?.();
|
|
97
|
-
}
|
|
98
|
-
catch {
|
|
99
|
-
// ignore
|
|
100
|
-
}
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
chunks.push(value);
|
|
104
|
-
bytes = nextBytes;
|
|
105
|
-
}
|
|
106
|
-
const buffer = concatChunks(chunks, bytes);
|
|
107
|
-
const text = decoder.decode(buffer);
|
|
108
|
-
return { text, bytes, truncated };
|
|
109
|
-
}
|
|
110
|
-
function formatFetchOutput(result, bodyText) {
|
|
111
|
-
const statusLine = `HTTP ${result.status} ${result.statusText}`.trim();
|
|
112
|
-
const meta = [
|
|
113
|
-
`url: ${result.url}`,
|
|
114
|
-
`bytes: ${result.bytes}${result.truncated ? " (truncated)" : ""}`,
|
|
115
|
-
`content-type: ${result.contentType ?? "unknown"}`,
|
|
116
|
-
];
|
|
117
|
-
return `${statusLine}\n${meta.join("\n")}\n\n${bodyText}`;
|
|
118
|
-
}
|
|
119
|
-
const fetchParamsSchema = Type.Object({
|
|
120
|
-
url: Type.String({ description: "HTTP or HTTPS URL" }),
|
|
121
|
-
method: Type.Optional(Type.String({ description: "HTTP method (default: GET)" })),
|
|
122
|
-
headers: Type.Optional(Type.Array(Type.Object({
|
|
123
|
-
name: Type.String({ description: "Header name" }),
|
|
124
|
-
value: Type.String({ description: "Header value" }),
|
|
125
|
-
}), { description: "Request headers as name/value pairs" })),
|
|
126
|
-
body: Type.Optional(Type.String({ description: "Request body (string)" })),
|
|
127
|
-
timeoutMs: Type.Optional(Type.Integer({ description: "Timeout in milliseconds", minimum: 1000, maximum: 120000 })),
|
|
128
|
-
maxBytes: Type.Optional(Type.Integer({ description: "Max response bytes", minimum: 1024, maximum: 5000000 })),
|
|
129
|
-
});
|
|
130
|
-
function createFetchTool() {
|
|
131
|
-
return {
|
|
132
|
-
name: "fetch",
|
|
133
|
-
label: "fetch",
|
|
134
|
-
description: "Fetch a URL via HTTP(S) and return the response body.",
|
|
135
|
-
parameters: fetchParamsSchema,
|
|
136
|
-
execute: async (toolCallId, params, onUpdate, _ctx, signal) => {
|
|
137
|
-
if (!isHttpUrl(params.url)) {
|
|
138
|
-
return {
|
|
139
|
-
content: [{ type: "text", text: "Invalid URL. Only http(s) is allowed." }],
|
|
140
|
-
details: {
|
|
141
|
-
url: params.url,
|
|
142
|
-
status: 0,
|
|
143
|
-
statusText: "invalid_url",
|
|
144
|
-
bytes: 0,
|
|
145
|
-
truncated: false,
|
|
146
|
-
contentType: null,
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
|
|
151
|
-
const maxBytes = resolveMaxBytes(params.maxBytes);
|
|
152
|
-
const controller = new AbortController();
|
|
153
|
-
const onAbort = () => controller.abort();
|
|
154
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
155
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
156
|
-
try {
|
|
157
|
-
onUpdate?.({
|
|
158
|
-
content: [{ type: "text", text: `Fetching ${params.url}...` }],
|
|
159
|
-
details: {
|
|
160
|
-
url: params.url,
|
|
161
|
-
status: 0,
|
|
162
|
-
statusText: "pending",
|
|
163
|
-
bytes: 0,
|
|
164
|
-
truncated: false,
|
|
165
|
-
contentType: null,
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
const response = await fetch(params.url, {
|
|
169
|
-
method: params.method?.toUpperCase() ?? "GET",
|
|
170
|
-
headers: normalizeHeaders(params.headers),
|
|
171
|
-
body: params.body,
|
|
172
|
-
signal: controller.signal,
|
|
173
|
-
});
|
|
174
|
-
const readResult = await readBodyLimited(response, maxBytes, signal);
|
|
175
|
-
const contentType = response.headers.get("content-type");
|
|
176
|
-
const output = formatFetchOutput({
|
|
177
|
-
url: response.url,
|
|
178
|
-
status: response.status,
|
|
179
|
-
statusText: response.statusText,
|
|
180
|
-
bytes: readResult.bytes,
|
|
181
|
-
truncated: readResult.truncated,
|
|
182
|
-
contentType,
|
|
183
|
-
}, readResult.text);
|
|
184
|
-
return {
|
|
185
|
-
content: [{ type: "text", text: output }],
|
|
186
|
-
details: {
|
|
187
|
-
url: response.url,
|
|
188
|
-
status: response.status,
|
|
189
|
-
statusText: response.statusText,
|
|
190
|
-
bytes: readResult.bytes,
|
|
191
|
-
truncated: readResult.truncated,
|
|
192
|
-
contentType,
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
198
|
-
return {
|
|
199
|
-
content: [{ type: "text", text: `Fetch failed: ${message}` }],
|
|
200
|
-
details: {
|
|
201
|
-
url: params.url,
|
|
202
|
-
status: 0,
|
|
203
|
-
statusText: "error",
|
|
204
|
-
bytes: 0,
|
|
205
|
-
truncated: false,
|
|
206
|
-
contentType: null,
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
finally {
|
|
211
|
-
clearTimeout(timeout);
|
|
212
|
-
signal?.removeEventListener("abort", onAbort);
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
const mcpParamsSchema = Type.Object({
|
|
218
|
-
server: Type.Optional(Type.String({ description: "MCP server name" })),
|
|
219
|
-
method: Type.String({ description: "MCP method name" }),
|
|
220
|
-
params: Type.Optional(Type.Any({ description: "MCP params payload" })),
|
|
221
|
-
});
|
|
222
|
-
function formatMcpToolOutput(server, result, bodyText) {
|
|
223
|
-
const statusLine = result.status > 0 ? `HTTP ${result.status} ${result.statusText}`.trim() : result.statusText;
|
|
224
|
-
const meta = [
|
|
225
|
-
`server: ${server.name}`,
|
|
226
|
-
`type: ${server.type}`,
|
|
227
|
-
`target: ${result.target}`,
|
|
228
|
-
`bytes: ${result.bytes}${result.truncated ? " (truncated)" : ""}`,
|
|
229
|
-
`content-type: ${result.contentType ?? "unknown"}`,
|
|
230
|
-
];
|
|
231
|
-
return `${statusLine}\n${meta.join("\n")}\n\n${bodyText}`;
|
|
232
|
-
}
|
|
233
|
-
function createMcpTool(loadServers) {
|
|
234
|
-
return {
|
|
235
|
-
name: "mcp",
|
|
236
|
-
label: "mcp",
|
|
237
|
-
description: "Call an MCP endpoint via JSON-RPC.",
|
|
238
|
-
parameters: mcpParamsSchema,
|
|
239
|
-
execute: async (_toolCallId, params, onUpdate, _ctx, signal) => {
|
|
240
|
-
const servers = await loadServers();
|
|
241
|
-
if (servers.length === 0) {
|
|
242
|
-
return {
|
|
243
|
-
content: [
|
|
244
|
-
{
|
|
245
|
-
type: "text",
|
|
246
|
-
text: "MCP is not configured. Add [mcp_servers.*] to ~/.tg-agent/config.toml.",
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
|
-
details: {
|
|
250
|
-
server: "",
|
|
251
|
-
type: "http",
|
|
252
|
-
target: "",
|
|
253
|
-
status: 0,
|
|
254
|
-
statusText: "not_configured",
|
|
255
|
-
bytes: 0,
|
|
256
|
-
truncated: false,
|
|
257
|
-
contentType: null,
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
let serverName = params.server?.trim() ?? "";
|
|
262
|
-
if (!serverName) {
|
|
263
|
-
if (servers.length === 1) {
|
|
264
|
-
serverName = servers[0]?.name ?? "";
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
return {
|
|
268
|
-
content: [
|
|
269
|
-
{
|
|
270
|
-
type: "text",
|
|
271
|
-
text: `Multiple MCP servers configured. Provide server name. Available: ${servers
|
|
272
|
-
.map((entry) => entry.name)
|
|
273
|
-
.join(", ")}`,
|
|
274
|
-
},
|
|
275
|
-
],
|
|
276
|
-
details: {
|
|
277
|
-
server: "",
|
|
278
|
-
type: "http",
|
|
279
|
-
target: "",
|
|
280
|
-
status: 0,
|
|
281
|
-
statusText: "server_required",
|
|
282
|
-
bytes: 0,
|
|
283
|
-
truncated: false,
|
|
284
|
-
contentType: null,
|
|
285
|
-
},
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const server = servers.find((entry) => entry.name === serverName);
|
|
290
|
-
if (!server) {
|
|
291
|
-
return {
|
|
292
|
-
content: [
|
|
293
|
-
{
|
|
294
|
-
type: "text",
|
|
295
|
-
text: `MCP server not found: ${serverName}. Available: ${servers
|
|
296
|
-
.map((entry) => entry.name)
|
|
297
|
-
.join(", ")}`,
|
|
298
|
-
},
|
|
299
|
-
],
|
|
300
|
-
details: {
|
|
301
|
-
server: serverName,
|
|
302
|
-
type: "http",
|
|
303
|
-
target: "",
|
|
304
|
-
status: 0,
|
|
305
|
-
statusText: "server_not_found",
|
|
306
|
-
bytes: 0,
|
|
307
|
-
truncated: false,
|
|
308
|
-
contentType: null,
|
|
309
|
-
},
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
onUpdate?.({
|
|
314
|
-
content: [
|
|
315
|
-
{
|
|
316
|
-
type: "text",
|
|
317
|
-
text: `Calling MCP ${server.name} ${params.method}...`,
|
|
318
|
-
},
|
|
319
|
-
],
|
|
320
|
-
details: {
|
|
321
|
-
server: server.name,
|
|
322
|
-
type: server.type,
|
|
323
|
-
target: formatMcpTarget(server),
|
|
324
|
-
status: 0,
|
|
325
|
-
statusText: "pending",
|
|
326
|
-
bytes: 0,
|
|
327
|
-
truncated: false,
|
|
328
|
-
contentType: null,
|
|
329
|
-
},
|
|
330
|
-
});
|
|
331
|
-
const result = await callMcpServer(server, params.method, params.params ?? {}, {
|
|
332
|
-
timeoutMs: resolveTimeoutMs(undefined),
|
|
333
|
-
maxBytes: resolveMaxBytes(undefined),
|
|
334
|
-
signal,
|
|
335
|
-
});
|
|
336
|
-
const output = formatMcpToolOutput(server, {
|
|
337
|
-
server: server.name,
|
|
338
|
-
type: server.type,
|
|
339
|
-
target: formatMcpTarget(server),
|
|
340
|
-
status: result.statusCode ?? 0,
|
|
341
|
-
statusText: result.statusText ?? (result.ok ? "ok" : "error"),
|
|
342
|
-
bytes: result.bytes,
|
|
343
|
-
truncated: result.truncated,
|
|
344
|
-
contentType: result.contentType ?? null,
|
|
345
|
-
}, result.output);
|
|
346
|
-
return {
|
|
347
|
-
content: [{ type: "text", text: output }],
|
|
348
|
-
details: {
|
|
349
|
-
server: server.name,
|
|
350
|
-
type: server.type,
|
|
351
|
-
target: formatMcpTarget(server),
|
|
352
|
-
status: result.statusCode ?? 0,
|
|
353
|
-
statusText: result.statusText ?? (result.ok ? "ok" : "error"),
|
|
354
|
-
bytes: result.bytes,
|
|
355
|
-
truncated: result.truncated,
|
|
356
|
-
contentType: result.contentType ?? null,
|
|
357
|
-
},
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
catch (error) {
|
|
361
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
362
|
-
return {
|
|
363
|
-
content: [{ type: "text", text: `MCP failed: ${message}` }],
|
|
364
|
-
details: {
|
|
365
|
-
server: server.name,
|
|
366
|
-
type: server.type,
|
|
367
|
-
target: formatMcpTarget(server),
|
|
368
|
-
status: 0,
|
|
369
|
-
statusText: "error",
|
|
370
|
-
bytes: 0,
|
|
371
|
-
truncated: false,
|
|
372
|
-
contentType: null,
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
export function createCustomTools() {
|
|
380
|
-
const tools = [createFetchTool()];
|
|
381
|
-
const servers = loadMcpServersSync(config.agentDir);
|
|
382
|
-
if (servers.length > 0) {
|
|
383
|
-
tools.push(createMcpTool(() => loadMcpServers(config.agentDir)));
|
|
384
|
-
}
|
|
385
|
-
return tools;
|
|
386
|
-
}
|
|
1
|
+
import f from"node:path";import $ from"node:fs/promises";import{Type as o}from"@sinclair/typebox";import{config as y}from"./config.js";import{callMcpServer as w,formatMcpTarget as x,loadMcpServers as S,loadMcpServersSync as _}from"./mcp.js";import{expandHome as P}from"./utils.js";const C=2e5,k=6e4;function b(e,t,r){return Number.isNaN(e)?t:Math.min(r,Math.max(t,e))}function O(e){if(!e)return{};const t={};if(Array.isArray(e)){for(const r of e)r?.name&&(t[r.name]=r.value??"");return t}for(const[r,s]of Object.entries(e))r&&(t[r]=s);return t}function T(e){const t=y.fetchTimeoutMs>0?y.fetchTimeoutMs:k;return b(e||t,1e3,12e4)}function g(e){const t=y.fetchMaxBytes>0?y.fetchMaxBytes:C;return b(e||t,1024,5e6)}function v(e,t,r){const s=e.trim();if(!s)return null;const n=P(s),l=f.isAbsolute(n)?n:f.join(r,n),u=f.resolve(l);for(const d of t){const c=f.resolve(d);if(u===c||u.startsWith(`${c}${f.sep}`))return u}return null}async function M(e){try{const t=await $.stat(e);return t.isFile()?{bytes:t.size}:null}catch{return null}}function F(e){try{const t=new URL(e);return t.protocol==="http:"||t.protocol==="https:"}catch{return!1}}function A(e,t){const r=new Uint8Array(t);let s=0;for(const n of e)r.set(n,s),s+=n.byteLength;return r}async function j(e,t,r){const s=e.body?.getReader?.();if(!s){const i=await e.text?.()??"",p=Math.min(i.length,t),m=i.length>t;return{text:i.slice(0,t),bytes:p,truncated:m}}const n=new TextDecoder("utf-8"),l=[];let u=0,d=!1;for(;;){if(r?.aborted){try{await s.cancel?.()}catch{}throw new Error("Fetch aborted")}const{done:i,value:p}=await s.read();if(i)break;if(!p)continue;const m=u+p.byteLength;if(m>t){const h=Math.max(0,t-u);h>0&&(l.push(p.slice(0,h)),u+=h),d=!0;try{await s.cancel?.()}catch{}break}l.push(p),u=m}const c=A(l,u);return{text:n.decode(c),bytes:u,truncated:d}}function L(e,t){const r=`HTTP ${e.status} ${e.statusText}`.trim(),s=[`url: ${e.url}`,`bytes: ${e.bytes}${e.truncated?" (truncated)":""}`,`content-type: ${e.contentType??"unknown"}`];return`${r}
|
|
2
|
+
${s.join(`
|
|
3
|
+
`)}
|
|
4
|
+
|
|
5
|
+
${t}`}const D=o.Object({url:o.String({description:"HTTP or HTTPS URL"}),method:o.Optional(o.String({description:"HTTP method (default: GET)"})),headers:o.Optional(o.Array(o.Object({name:o.String({description:"Header name"}),value:o.String({description:"Header value"})}),{description:"Request headers as name/value pairs"})),body:o.Optional(o.String({description:"Request body (string)"})),timeoutMs:o.Optional(o.Integer({description:"Timeout in milliseconds",minimum:1e3,maximum:12e4})),maxBytes:o.Optional(o.Integer({description:"Max response bytes",minimum:1024,maximum:5e6}))});function E(){return{name:"fetch",label:"fetch",description:"Fetch a URL via HTTP(S) and return the response body.",parameters:D,execute:async(e,t,r,s,n)=>{if(!F(t.url))return{content:[{type:"text",text:"Invalid URL. Only http(s) is allowed."}],details:{url:t.url,status:0,statusText:"invalid_url",bytes:0,truncated:!1,contentType:null}};const l=T(t.timeoutMs),u=g(t.maxBytes),d=new AbortController,c=()=>d.abort();n?.addEventListener("abort",c,{once:!0});const a=setTimeout(()=>d.abort(),l);try{r?.({content:[{type:"text",text:`Fetching ${t.url}...`}],details:{url:t.url,status:0,statusText:"pending",bytes:0,truncated:!1,contentType:null}});const i=await fetch(t.url,{method:t.method?.toUpperCase()??"GET",headers:O(t.headers),body:t.body,signal:d.signal}),p=await j(i,u,n),m=i.headers.get("content-type");return{content:[{type:"text",text:L({url:i.url,status:i.status,statusText:i.statusText,bytes:p.bytes,truncated:p.truncated,contentType:m},p.text)}],details:{url:i.url,status:i.status,statusText:i.statusText,bytes:p.bytes,truncated:p.truncated,contentType:m}}}catch(i){return{content:[{type:"text",text:`Fetch failed: ${i instanceof Error?i.message:String(i)}`}],details:{url:t.url,status:0,statusText:"error",bytes:0,truncated:!1,contentType:null}}}finally{clearTimeout(a),n?.removeEventListener("abort",c)}}}}const H=o.Object({server:o.Optional(o.String({description:"MCP server name"})),method:o.String({description:"MCP method name"}),params:o.Optional(o.Any({description:"MCP params payload"}))});function I(e,t,r){const s=t.status>0?`HTTP ${t.status} ${t.statusText}`.trim():t.statusText,n=[`server: ${e.name}`,`type: ${e.type}`,`target: ${t.target}`,`bytes: ${t.bytes}${t.truncated?" (truncated)":""}`,`content-type: ${t.contentType??"unknown"}`];return`${s}
|
|
6
|
+
${n.join(`
|
|
7
|
+
`)}
|
|
8
|
+
|
|
9
|
+
${r}`}function R(e){return{name:"mcp",label:"mcp",description:"Call an MCP endpoint via JSON-RPC.",parameters:H,execute:async(t,r,s,n,l)=>{const u=await e();if(u.length===0)return{content:[{type:"text",text:"MCP is not configured. Add [mcp_servers.*] to ~/.tg-agent/config.toml."}],details:{server:"",type:"http",target:"",status:0,statusText:"not_configured",bytes:0,truncated:!1,contentType:null}};let d=r.server?.trim()??"";if(!d)if(u.length===1)d=u[0]?.name??"";else return{content:[{type:"text",text:`Multiple MCP servers configured. Provide server name. Available: ${u.map(a=>a.name).join(", ")}`}],details:{server:"",type:"http",target:"",status:0,statusText:"server_required",bytes:0,truncated:!1,contentType:null}};const c=u.find(a=>a.name===d);if(!c)return{content:[{type:"text",text:`MCP server not found: ${d}. Available: ${u.map(a=>a.name).join(", ")}`}],details:{server:d,type:"http",target:"",status:0,statusText:"server_not_found",bytes:0,truncated:!1,contentType:null}};try{s?.({content:[{type:"text",text:`Calling MCP ${c.name} ${r.method}...`}],details:{server:c.name,type:c.type,target:x(c),status:0,statusText:"pending",bytes:0,truncated:!1,contentType:null}});const a=await w(c,r.method,r.params??{},{timeoutMs:T(void 0),maxBytes:g(void 0),signal:l});return{content:[{type:"text",text:I(c,{server:c.name,type:c.type,target:x(c),status:a.statusCode??0,statusText:a.statusText??(a.ok?"ok":"error"),bytes:a.bytes,truncated:a.truncated,contentType:a.contentType??null},a.output)}],details:{server:c.name,type:c.type,target:x(c),status:a.statusCode??0,statusText:a.statusText??(a.ok?"ok":"error"),bytes:a.bytes,truncated:a.truncated,contentType:a.contentType??null}}}catch(a){return{content:[{type:"text",text:`MCP failed: ${a instanceof Error?a.message:String(a)}`}],details:{server:c.name,type:c.type,target:x(c),status:0,statusText:"error",bytes:0,truncated:!1,contentType:null}}}}}}const U=o.Object({path:o.String({description:"Image file path (relative to workspace or uploads)."}),caption:o.Optional(o.String({description:"Caption text (plain text)."}))}),B=o.Object({path:o.String({description:"File path (relative to workspace or uploads)."}),caption:o.Optional(o.String({description:"Caption text (plain text)."}))});function N(e){const t=[y.workspaceDir,f.join(y.agentDir,"uploads")];return{name:"send_photo",label:"send_photo",description:"Send an image file to the current Telegram chat.",parameters:U,execute:async(r,s)=>{const n=v(s.path,t,y.workspaceDir);if(!n)return{content:[{type:"text",text:"Invalid path. Only workspace or uploads are allowed."}],details:{path:s.path,bytes:0}};const l=await M(n);return l?(await e.sendPhoto(n,s.caption?.trim()||void 0),{content:[{type:"text",text:`Photo sent: ${f.basename(n)}`}],details:{path:n,bytes:l.bytes}}):{content:[{type:"text",text:`File not found: ${n}`}],details:{path:n,bytes:0}}}}}function z(e){const t=[y.workspaceDir,f.join(y.agentDir,"uploads")];return{name:"send_file",label:"send_file",description:"Send a file to the current Telegram chat.",parameters:B,execute:async(r,s)=>{const n=v(s.path,t,y.workspaceDir);if(!n)return{content:[{type:"text",text:"Invalid path. Only workspace or uploads are allowed."}],details:{path:s.path,bytes:0}};const l=await M(n);return l?(await e.sendDocument(n,s.caption?.trim()||void 0),{content:[{type:"text",text:`File sent: ${f.basename(n)}`}],details:{path:n,bytes:l.bytes}}):{content:[{type:"text",text:`File not found: ${n}`}],details:{path:n,bytes:0}}}}}function K(e){const t=[E()];return e?.telegram&&(t.push(N(e.telegram)),t.push(z(e.telegram))),_(y.agentDir).length>0&&t.push(R(()=>S(y.agentDir))),t}export{K as createCustomTools};
|