tg-agent 0.1.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/.env.example ADDED
@@ -0,0 +1,50 @@
1
+ # Required
2
+ TELEGRAM_BOT_TOKEN=
3
+
4
+ # Optional allowlist (comma-separated user IDs)
5
+ TELEGRAM_ALLOWED_USER_IDS=
6
+
7
+ # Codex auth location (optional)
8
+ CODEX_HOME=~/.codex
9
+
10
+ # Runtime dirs
11
+ AGENT_DIR=~/.tg-agent
12
+ WORKSPACE_DIR=~/
13
+ SESSION_DIR=~/.codex/tg-sessions
14
+
15
+ # Model selection
16
+ LLM_PROVIDER=openai-codex
17
+ LLM_MODEL=gpt-5.2
18
+ OPENAI_API_KEY=
19
+
20
+ # Limits
21
+ MAX_SESSIONS=3
22
+ MAX_CONCURRENT=3
23
+ SESSION_TTL_MIN=60
24
+ MAX_HISTORY_MESSAGES=40
25
+
26
+ # Timeouts
27
+ MODEL_TIMEOUT_MS=60000
28
+ MODEL_TIMEOUT_STREAM_MS=300000
29
+
30
+ # Fetch tool
31
+ FETCH_MAX_BYTES=200000
32
+ FETCH_TIMEOUT_MS=60000
33
+ FETCH_PROXY_URL=
34
+
35
+ # Proxy for model + global traffic (or use standard HTTP(S)_PROXY / ALL_PROXY)
36
+ PROXY_URL=
37
+
38
+ # MCP
39
+
40
+ # TLS (optional)
41
+ NODE_EXTRA_CA_CERTS=
42
+ NODE_TLS_REJECT_UNAUTHORIZED=
43
+
44
+ # Logging
45
+ LOG_AGENT_EVENTS=1
46
+ LOG_AGENT_STREAM=0
47
+
48
+ # Output / prompt
49
+ SYSTEM_PROMPT=You are Codex running locally. Be concise and practical.
50
+ TELEGRAM_PARSE_MODE=
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # tg-agent
2
+
3
+ 一个在本地运行的 Telegram DM 机器人,用 Codex 账号驱动的 agent 来处理对话、工具调用与会话管理。支持多会话、会话持久化、/stop 中断、工具状态回写,适合在 macOS 上长期跑。
4
+
5
+ ## 功能
6
+
7
+ - Telegram DM 交互(仅私聊)
8
+ - 多会话管理:/new /list /use /close /reset
9
+ - 会话持久化:默认保存到 `~/.codex/tg-sessions`
10
+ - 运行中可取消:/stop
11
+ - 自动 Markdown 渲染:优先 MarkdownV2,失败回退 Markdown,再回退纯文本
12
+ - 多 Provider / 多 Model 选择(支持 Codex / antigravity / Gemini CLI 等)
13
+ - OAuth 登录与注销(/login, /logout)
14
+ - 工具调用:内置 `fetch`,可选 MCP(HTTP/stdio)
15
+ - 代理支持:模型与工具请求可分别配置
16
+
17
+ ## 快速开始
18
+
19
+ 1) 安装依赖
20
+
21
+ ```bash
22
+ npm install
23
+ ```
24
+
25
+ 2) 配置环境变量
26
+
27
+ 复制并修改 `.env`:
28
+
29
+ ```bash
30
+ cp .env.example .env
31
+ ```
32
+
33
+ 最小必需配置示例:
34
+
35
+ ```env
36
+ TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN
37
+ TELEGRAM_ALLOWED_USER_IDS=123456789
38
+ PROXY_URL=socks5h://127.0.0.1:7890
39
+ LLM_PROVIDER=openai-codex
40
+ LLM_MODEL=codex-latest
41
+ ```
42
+
43
+ 3) 运行
44
+
45
+ ```bash
46
+ npm run dev
47
+ ```
48
+
49
+ 或构建后运行:
50
+
51
+ ```bash
52
+ npm run build
53
+ npm start
54
+ ```
55
+
56
+ ## 使用说明
57
+
58
+ 在 Telegram 私聊中发送:
59
+
60
+ - `/new [title]` 创建会话
61
+ - `/list` 列出会话
62
+ - `/use <id>` 切换会话
63
+ - `/close [id]` 关闭会话
64
+ - `/reset` 清空当前会话历史
65
+ - `/stop` 终止当前正在处理的请求
66
+ - `/providers` 列出可用 Provider
67
+ - `/models [provider]` 列出模型
68
+ - `/provider <name>` 设置当前会话 Provider
69
+ - `/model <provider>/<model>` 设置当前会话模型
70
+ - `/mcp` 查看 MCP 服务器与连接状态
71
+ - `/status` 查看当前会话模型设置
72
+ - `/login <provider>` OAuth 登录
73
+ - `/logout <provider>` 退出登录
74
+
75
+ 默认仅支持私聊(DM)。
76
+
77
+ ## 环境变量
78
+
79
+ `.env.example` 已列出所有可配置项。常用说明如下:
80
+
81
+ - `TELEGRAM_BOT_TOKEN`:必填
82
+ - `TELEGRAM_ALLOWED_USER_IDS`:可选,逗号分隔白名单用户 ID
83
+ - `AGENT_DIR`:agent 配置与缓存目录(默认 `~/.tg-agent`)
84
+ - `WORKSPACE_DIR`:工具操作的工作目录
85
+ - `SESSION_DIR`:会话持久化目录
86
+ - `LLM_PROVIDER`:模型提供方(默认 `openai-codex`)
87
+ - `LLM_MODEL`:模型 ID,例如 `codex-latest`
88
+ - `PROXY_URL`:模型与全局流量代理
89
+ - `FETCH_PROXY_URL`:仅用于 `fetch` 工具的代理(必须是 HTTP/HTTPS)
90
+ MCP 配置通过 `~/.tg-agent/config.toml` 提供,示例见下。
91
+
92
+ ## Codex 认证
93
+
94
+ 优先使用本机 Codex OAuth 凭据(`codex login` 写入的配置)。
95
+ 也可以在 Telegram 内执行 `/login openai-codex`,将凭据保存到 `AGENT_DIR/auth.json`。
96
+ 如果运行时提示缺少权限或无法找到凭据,请重新执行:
97
+
98
+ ```bash
99
+ codex login
100
+ ```
101
+
102
+ ## MCP 配置
103
+
104
+ 在 `~/.tg-agent/config.toml` 中添加 MCP 服务器:
105
+
106
+ ```toml
107
+ [mcp_servers.context7]
108
+ type = "stdio"
109
+ command = "npx"
110
+ args = ["-y", "@upstash/context7-mcp@latest"]
111
+
112
+ [mcp_servers.Notion]
113
+ url = "https://mcp.notion.com/mcp"
114
+ ```
115
+
116
+ ## 打包发布
117
+
118
+ 构建并生成可执行入口:
119
+
120
+ ```bash
121
+ npm run build
122
+ ```
123
+
124
+ 本地验证打包内容:
125
+
126
+ ```bash
127
+ npm pack
128
+ ```
129
+
130
+ 发布到 npm(可选):
131
+
132
+ ```bash
133
+ npm publish
134
+ ```
135
+
136
+ 安装后可直接使用:
137
+
138
+ ```bash
139
+ tg-agent
140
+ ```
141
+
142
+ ## 常见问题
143
+
144
+ - `Bad Request: can't parse entities`: 说明文本不符合 MarkdownV2 规则,当前实现会自动降级。
145
+ - `No Codex OAuth credentials found`: 执行 `codex login`。
146
+ - `insufficient permissions`: 检查组织/项目权限或 OAuth scope。
147
+ - 网络错误:检查代理配置与 TLS 证书设置。
148
+
149
+ ## 安全说明
150
+
151
+ - 不要提交 `.env` 到仓库。
152
+ - 建议开启 `TELEGRAM_ALLOWED_USER_IDS` 白名单。
package/dist/auth.js ADDED
@@ -0,0 +1,71 @@
1
+ import { config } from "./config.js";
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
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./index.js";
@@ -0,0 +1,93 @@
1
+ import { createHash } from "node:crypto";
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
+ }
package/dist/config.js ADDED
@@ -0,0 +1,59 @@
1
+ import path from "node:path";
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
+ }