@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 CHANGED
@@ -39,6 +39,7 @@ export declare class CcTgBot {
39
39
  private handleMcpVersion;
40
40
  private handleClearNpxCache;
41
41
  private handleRestart;
42
+ private handleGetFile;
42
43
  private killSession;
43
44
  stop(): void;
44
45
  }
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 { execSync, spawn } from "child_process";
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
- // Write tool uses file_path, Edit uses file_path
343
- const filePath = input.file_path ?? input.path;
344
- if (!filePath)
345
- continue;
346
- // Resolve relative paths against cwd
347
- const resolved = filePath.startsWith("/")
348
- ? filePath
349
- : resolve(cwd ?? process.cwd(), filePath);
350
- console.log(`[claude:files] tracked written file: ${resolved}`);
351
- session.writtenFiles.add(resolved);
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 (session.writtenFiles.has(resolved) && existsSync(resolved)) {
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 bot... brb.");
617
- await new Promise(resolve => setTimeout(resolve, 1000));
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gonzih/cc-tg",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "Claude Code Telegram bot — chat with Claude Code via Telegram",
5
5
  "type": "module",
6
6
  "bin": {