@gonzih/cc-tg 0.2.10 → 0.2.12
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/dist/bot.d.ts +1 -0
- package/dist/bot.js +99 -40
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
package/dist/bot.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import TelegramBot from "node-telegram-bot-api";
|
|
6
6
|
import { existsSync, createWriteStream, mkdirSync } from "fs";
|
|
7
7
|
import { resolve, basename, join } from "path";
|
|
8
|
-
import
|
|
8
|
+
import os from "os";
|
|
9
|
+
import { execSync } from "child_process";
|
|
9
10
|
import https from "https";
|
|
10
11
|
import http from "http";
|
|
11
12
|
import { ClaudeProcess, extractText } from "./claude.js";
|
|
@@ -22,6 +23,7 @@ const BOT_COMMANDS = [
|
|
|
22
23
|
{ command: "mcp_version", description: "Show cc-agent npm version and npx cache info" },
|
|
23
24
|
{ command: "clear_npx_cache", description: "Clear npx cache and restart MCP to pick up latest version" },
|
|
24
25
|
{ command: "restart", description: "Restart the bot process in-place" },
|
|
26
|
+
{ command: "get_file", description: "Get a file from the server by path" },
|
|
25
27
|
];
|
|
26
28
|
const FLUSH_DELAY_MS = 800; // debounce streaming chunks into one Telegram message
|
|
27
29
|
const TYPING_INTERVAL_MS = 4000; // re-send typing action before Telegram's 5s expiry
|
|
@@ -137,6 +139,11 @@ export class CcTgBot {
|
|
|
137
139
|
await this.handleRestart(chatId);
|
|
138
140
|
return;
|
|
139
141
|
}
|
|
142
|
+
// /get_file <path> — send a file from the server to the user
|
|
143
|
+
if (text.startsWith("/get_file")) {
|
|
144
|
+
await this.handleGetFile(chatId, text);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
140
147
|
const session = this.getOrCreateSession(chatId);
|
|
141
148
|
try {
|
|
142
149
|
session.claude.sendPrompt(text);
|
|
@@ -334,21 +341,44 @@ export class CcTgBot {
|
|
|
334
341
|
if (block.type !== "tool_use")
|
|
335
342
|
continue;
|
|
336
343
|
const name = block.name;
|
|
337
|
-
if (!["Write", "Edit", "NotebookEdit"].includes(name))
|
|
338
|
-
continue;
|
|
339
344
|
const input = block.input;
|
|
340
345
|
if (!input)
|
|
341
346
|
continue;
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
347
|
+
if (["Write", "Edit", "NotebookEdit"].includes(name)) {
|
|
348
|
+
// Write tool uses file_path, Edit uses file_path
|
|
349
|
+
const filePath = input.file_path ?? input.path;
|
|
350
|
+
if (!filePath)
|
|
351
|
+
continue;
|
|
352
|
+
// Resolve relative paths against cwd
|
|
353
|
+
const resolved = filePath.startsWith("/")
|
|
354
|
+
? filePath
|
|
355
|
+
: resolve(cwd ?? process.cwd(), filePath);
|
|
356
|
+
console.log(`[claude:files] tracked written file: ${resolved}`);
|
|
357
|
+
session.writtenFiles.add(resolved);
|
|
358
|
+
}
|
|
359
|
+
else if (name === "Bash") {
|
|
360
|
+
const cmd = input.command ?? "";
|
|
361
|
+
// yt-dlp / ffmpeg -o "path"
|
|
362
|
+
const oFlag = cmd.match(/-o\s+["']?([^\s"']+\.[\w]{1,10})["']?/);
|
|
363
|
+
if (oFlag)
|
|
364
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), oFlag[1]));
|
|
365
|
+
// mv source dest — track dest
|
|
366
|
+
const mvMatch = cmd.match(/\bmv\s+\S+\s+["']?([^\s"']+)["']?$/);
|
|
367
|
+
if (mvMatch)
|
|
368
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), mvMatch[1]));
|
|
369
|
+
// cp source dest — track dest
|
|
370
|
+
const cpMatch = cmd.match(/\bcp\s+\S+\s+["']?([^\s"']+)["']?$/);
|
|
371
|
+
if (cpMatch)
|
|
372
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), cpMatch[1]));
|
|
373
|
+
// curl -o path or wget -O path
|
|
374
|
+
const curlMatch = cmd.match(/curl\s+.*?-o\s+["']?([^\s"']+)["']?/);
|
|
375
|
+
if (curlMatch)
|
|
376
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), curlMatch[1]));
|
|
377
|
+
// wget -O path
|
|
378
|
+
const wgetMatch = cmd.match(/wget\s+.*?-O\s+["']?([^\s"']+)["']?/);
|
|
379
|
+
if (wgetMatch)
|
|
380
|
+
session.writtenFiles.add(resolve(cwd ?? process.cwd(), wgetMatch[1]));
|
|
381
|
+
}
|
|
352
382
|
}
|
|
353
383
|
}
|
|
354
384
|
isSensitiveFile(filePath) {
|
|
@@ -362,8 +392,6 @@ export class CcTgBot {
|
|
|
362
392
|
return sensitivePatterns.some((p) => p.test(name));
|
|
363
393
|
}
|
|
364
394
|
uploadMentionedFiles(chatId, resultText, session) {
|
|
365
|
-
if (session.writtenFiles.size === 0)
|
|
366
|
-
return;
|
|
367
395
|
// Extract file path candidates from result text
|
|
368
396
|
// Match: /absolute/path/file.ext or relative like ./foo/bar.csv or just foo.pdf
|
|
369
397
|
const pathPattern = /(?:^|[\s`'"(])(\/?[\w.\-/]+\.[\w]{1,10})(?:[\s`'")\n]|$)/gm;
|
|
@@ -372,24 +400,38 @@ export class CcTgBot {
|
|
|
372
400
|
while ((match = pathPattern.exec(resultText)) !== null) {
|
|
373
401
|
candidates.add(match[1]);
|
|
374
402
|
}
|
|
403
|
+
const safeDirs = ["/tmp/", "/var/folders/", os.homedir() + "/Downloads/"];
|
|
404
|
+
const isSafeDir = (p) => safeDirs.some(d => p.startsWith(d)) || p.startsWith(this.opts.cwd ?? process.cwd());
|
|
375
405
|
const toUpload = [];
|
|
406
|
+
if (session.writtenFiles.size > 0) {
|
|
407
|
+
for (const candidate of candidates) {
|
|
408
|
+
// Try as-is (absolute), or resolve against cwd
|
|
409
|
+
const resolved = candidate.startsWith("/")
|
|
410
|
+
? candidate
|
|
411
|
+
: resolve(this.opts.cwd ?? process.cwd(), candidate);
|
|
412
|
+
if (session.writtenFiles.has(resolved) && existsSync(resolved)) {
|
|
413
|
+
toUpload.push(resolved);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
// Also check by basename — result might mention just the filename
|
|
417
|
+
for (const written of session.writtenFiles) {
|
|
418
|
+
if (basename(written) === basename(candidate) && existsSync(written)) {
|
|
419
|
+
toUpload.push(written);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Also upload files mentioned in result text that exist in safe dirs
|
|
427
|
+
// even if not tracked via Write tool
|
|
376
428
|
for (const candidate of candidates) {
|
|
377
|
-
// Try as-is (absolute), or resolve against cwd
|
|
378
429
|
const resolved = candidate.startsWith("/")
|
|
379
430
|
? candidate
|
|
380
431
|
: resolve(this.opts.cwd ?? process.cwd(), candidate);
|
|
381
|
-
if (
|
|
432
|
+
if (existsSync(resolved) && isSafeDir(resolved) && !toUpload.includes(resolved)) {
|
|
382
433
|
toUpload.push(resolved);
|
|
383
434
|
}
|
|
384
|
-
else {
|
|
385
|
-
// Also check by basename — result might mention just the filename
|
|
386
|
-
for (const written of session.writtenFiles) {
|
|
387
|
-
if (basename(written) === basename(candidate) && existsSync(written)) {
|
|
388
|
-
toUpload.push(written);
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
435
|
}
|
|
394
436
|
// Deduplicate and filter sensitive files
|
|
395
437
|
const unique = [...new Set(toUpload)];
|
|
@@ -613,22 +655,39 @@ export class CcTgBot {
|
|
|
613
655
|
await this.bot.sendMessage(chatId, `NPX cache cleared and MCP restarted.${pidNote} Will pick up latest npm version on next call.`);
|
|
614
656
|
}
|
|
615
657
|
async handleRestart(chatId) {
|
|
616
|
-
await this.bot.sendMessage(chatId, "Restarting
|
|
617
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
618
|
-
this.stop();
|
|
619
|
-
const isLaunchd = process.ppid === 1;
|
|
620
|
-
if (!isLaunchd) {
|
|
621
|
-
// Running manually — spawn a replacement process
|
|
622
|
-
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
623
|
-
detached: true,
|
|
624
|
-
stdio: "ignore",
|
|
625
|
-
env: process.env,
|
|
626
|
-
});
|
|
627
|
-
child.unref();
|
|
628
|
-
}
|
|
629
|
-
// If launchd-managed, just exit — launchd will restart us
|
|
658
|
+
await this.bot.sendMessage(chatId, "Restarting... brb.");
|
|
659
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
630
660
|
process.exit(0);
|
|
631
661
|
}
|
|
662
|
+
async handleGetFile(chatId, text) {
|
|
663
|
+
const arg = text.slice("/get_file".length).trim();
|
|
664
|
+
if (!arg) {
|
|
665
|
+
await this.bot.sendMessage(chatId, "Usage: /get_file <path>");
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const filePath = resolve(arg);
|
|
669
|
+
const safeDirs = ["/tmp/", "/var/folders/", os.homedir() + "/Downloads/", this.opts.cwd ?? process.cwd()];
|
|
670
|
+
const inSafeDir = safeDirs.some(d => filePath.startsWith(d));
|
|
671
|
+
if (!inSafeDir) {
|
|
672
|
+
await this.bot.sendMessage(chatId, "Access denied: path not in allowed directories");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!existsSync(filePath)) {
|
|
676
|
+
await this.bot.sendMessage(chatId, `File not found: ${filePath}`);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const { statSync } = await import("fs");
|
|
680
|
+
const stat = statSync(filePath);
|
|
681
|
+
if (!stat.isFile()) {
|
|
682
|
+
await this.bot.sendMessage(chatId, `Not a file: ${filePath}`);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (this.isSensitiveFile(filePath)) {
|
|
686
|
+
await this.bot.sendMessage(chatId, "Access denied: sensitive file");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
await this.bot.sendDocument(chatId, filePath);
|
|
690
|
+
}
|
|
632
691
|
killSession(chatId, keepCrons = true) {
|
|
633
692
|
const session = this.sessions.get(chatId);
|
|
634
693
|
if (session) {
|