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 +50 -0
- package/README.md +152 -0
- package/dist/auth.js +71 -0
- package/dist/cli.js +2 -0
- package/dist/codexAuth.js +93 -0
- package/dist/config.js +59 -0
- package/dist/customTools.js +386 -0
- package/dist/index.js +954 -0
- package/dist/mcp.js +427 -0
- package/dist/piAgentRunner.js +407 -0
- package/dist/piAiRunner.js +99 -0
- package/dist/proxy.js +19 -0
- package/dist/sessionStore.js +138 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +91 -0
- package/package.json +41 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { config } from "./config.js";
|
|
3
|
+
import { callMcpServer, formatMcpTarget, loadMcpServers, loadMcpServersSync } from "./mcp.js";
|
|
4
|
+
const DEFAULT_MAX_BYTES = 200_000;
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
6
|
+
function clampNumber(value, min, max) {
|
|
7
|
+
if (Number.isNaN(value))
|
|
8
|
+
return min;
|
|
9
|
+
return Math.min(max, Math.max(min, value));
|
|
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
|
+
}
|