ccbot 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Claude Code Bot Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,158 @@
1
+ # πŸ€– ccbot β€” Claude Code ↔ Telegram Notification Bot
2
+
3
+ [TiαΊΏng Việt](./README.md)
4
+
5
+ > Get Telegram notifications when Claude Code completes a response β€” with git diff, processing time, and result summary.
6
+
7
+ ---
8
+
9
+ ## Problem
10
+
11
+ You're using Claude Code on your computer. You step away with your phone but have no idea if Claude Code is done yet or what files it changed.
12
+
13
+ **ccbot** is a lightweight bridge between Claude Code and Telegram β€” when Claude Code finishes, you get a notification right on your phone.
14
+
15
+ ```
16
+ Claude Code completes response
17
+ ↓
18
+ Stop Hook triggers
19
+ ↓
20
+ ccbot receives event
21
+ ↓
22
+ Telegram notification πŸ“±
23
+ ```
24
+
25
+ ## Features
26
+
27
+ - πŸ”” **Auto notification** β€” Claude Code finishes β†’ Telegram notifies you instantly
28
+ - πŸ“‚ **Git diff included** β€” see changed files without opening your computer
29
+ - ⏱ **Processing time** β€” know how long Claude Code took
30
+ - πŸ“ **Response summary** β€” quick glance at what Claude Code replied
31
+ - πŸ” **User whitelist** β€” only authorized users can use the bot
32
+ - πŸ“„ **Auto-split messages** β€” long responses are automatically paginated `[1/N]`
33
+
34
+ ## Requirements
35
+
36
+ - **Node.js** β‰₯ 18
37
+ - **pnpm** (or npm/yarn)
38
+ - **Telegram Bot Token** β€” create from [@BotFather](https://t.me/BotFather)
39
+ - **Telegram User ID** β€” get from [@userinfobot](https://t.me/userinfobot)
40
+
41
+ ## Getting Started
42
+
43
+ ### Option 1: Global install (recommended)
44
+
45
+ ```bash
46
+ pnpm add -g ccbot
47
+ ccbot setup
48
+ ```
49
+
50
+ ### Option 2: npx (no install needed)
51
+
52
+ ```bash
53
+ npx ccbot setup
54
+ ```
55
+
56
+ ### Option 3: Clone repo (for development)
57
+
58
+ ```bash
59
+ git clone https://github.com/palooza-kaida/ccbot.git
60
+ cd ccbot
61
+ pnpm install
62
+ pnpm setup
63
+ ```
64
+
65
+ The setup wizard will guide you step by step:
66
+
67
+ ```
68
+ β”Œ πŸ€– ccbot setup
69
+ β”‚
70
+ β—‡ Telegram Bot Token
71
+ β”‚ your-bot-token
72
+ β”‚
73
+ β—‡ Your Telegram User ID
74
+ β”‚ your-user-id
75
+ β”‚
76
+ β—† Config saved
77
+ β—† Hook installed β†’ ~/.claude/settings.json
78
+ β—† Chat ID registered
79
+ β”‚
80
+ β”” πŸŽ‰ Setup complete!
81
+ ```
82
+
83
+ <details>
84
+ <summary>Manual setup (without wizard)</summary>
85
+
86
+ Create `~/.ccbot/config.json`:
87
+
88
+ ```json
89
+ {
90
+ "telegram_bot_token": "123456:ABC-xxx",
91
+ "user_id": 123456789,
92
+ "hook_port": 9377
93
+ }
94
+ ```
95
+
96
+ Then run `ccbot setup` to install the hook and register your chat ID.
97
+
98
+ </details>
99
+
100
+ ## Usage
101
+
102
+ ### Start the bot
103
+
104
+ ```bash
105
+ # Global install
106
+ ccbot
107
+
108
+ # Or npx
109
+ npx ccbot
110
+
111
+ # Or local dev
112
+ pnpm dev
113
+ ```
114
+
115
+ Once running, use Claude Code as usual β†’ notifications will arrive on Telegram.
116
+
117
+ ### Telegram Commands
118
+
119
+ | Command | Description |
120
+ |-----------|-----------------------------------------------------|
121
+ | `/start` | Re-register chat (auto during setup, rarely needed) |
122
+ | `/ping` | Check if bot is alive |
123
+ | `/status` | View bot status |
124
+
125
+ ### Sample Notification
126
+
127
+ ```
128
+ πŸ€– Claude Code Response
129
+ πŸ“‚ my-project | ⏱ 45s
130
+
131
+ Fixed authentication bug in login.go. Main changes:
132
+ - Fix missing error check at line 42
133
+ - Add input validation...
134
+
135
+ πŸ“‚ Changes:
136
+ ✏️ src/login.go
137
+ βž• src/validator.go
138
+ ❌ src/old_auth.go
139
+ ```
140
+
141
+ ## Uninstall
142
+
143
+ ```bash
144
+ ccbot uninstall
145
+ ```
146
+
147
+ ```
148
+ β”Œ πŸ—‘οΈ Uninstalling ccbot
149
+ β”‚
150
+ β—† Hook removed from ~/.claude/settings.json
151
+ β—† Removed ~/.ccbot/ (config, state, hooks)
152
+ β”‚
153
+ β”” ccbot uninstalled
154
+ ```
155
+
156
+ ## License
157
+
158
+ MIT
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # πŸ€– ccbot β€” Claude Code ↔ Telegram Notification Bot
2
+
3
+ [English](./README.en.md)
4
+
5
+ > NhαΊ­n thΓ΄ng bΓ‘o Telegram khi Claude Code hoΓ n thΓ nh response β€” kΓ¨m git diff, thời gian xα»­ lΓ½, vΓ  tΓ³m tαΊ―t kαΊΏt quαΊ£.
6
+
7
+ ---
8
+
9
+ ## VαΊ₯n đề giαΊ£i quyαΊΏt
10
+
11
+ BαΊ‘n Δ‘ang dΓΉng Claude Code trΓͺn mΓ‘y tΓ­nh. Ra ngoΓ i cαΊ§m Δ‘iện thoαΊ‘i nhΖ°ng khΓ΄ng biαΊΏt Claude Code Δ‘Γ£ xong chΖ°a, thay Δ‘α»•i file nΓ o.
12
+
13
+ **ccbot** lΓ  cαΊ§u nα»‘i nhαΊΉ giα»―a Claude Code vΓ  Telegram β€” khi Claude Code xong việc, bαΊ‘n nhαΊ­n notification ngay trΓͺn Δ‘iện thoαΊ‘i.
14
+
15
+ ```
16
+ Claude Code xong response
17
+ ↓
18
+ Stop Hook trigger
19
+ ↓
20
+ ccbot nhαΊ­n event
21
+ ↓
22
+ Telegram notification πŸ“±
23
+ ```
24
+
25
+ ## Tính năng
26
+
27
+ - πŸ”” **Notification tα»± Δ‘α»™ng** β€” Claude Code xong β†’ Telegram nhαΊ­n tin ngay
28
+ - πŸ“‚ **Git diff kΓ¨m theo** β€” biαΊΏt file nΓ o thay Δ‘α»•i mΓ  khΓ΄ng cαΊ§n mở mΓ‘y tΓ­nh
29
+ - ⏱ **Thời gian xα»­ lΓ½** β€” biαΊΏt Claude Code chαΊ‘y bao lΓ’u
30
+ - πŸ“ **TΓ³m tαΊ―t response** β€” xem nhanh Claude Code trαΊ£ lời gΓ¬
31
+ - πŸ” **Whitelist user** β€” chỉ user được phΓ©p mα»›i dΓΉng được bot
32
+ - πŸ“„ **Auto-split message** β€” response dΓ i tα»± Δ‘α»™ng chia page `[1/N]`
33
+
34
+ ## YΓͺu cαΊ§u
35
+
36
+ - **Node.js** β‰₯ 18
37
+ - **pnpm** (hoαΊ·c npm/yarn)
38
+ - **Telegram Bot Token** β€” tαΊ‘o tα»« [@BotFather](https://t.me/BotFather)
39
+ - **Telegram User ID** β€” lαΊ₯y tα»« [@userinfobot](https://t.me/userinfobot)
40
+
41
+ ## BαΊ―t Δ‘αΊ§u
42
+
43
+ ### CΓ‘ch 1: Global install (khuyαΊΏn nghα»‹)
44
+
45
+ ```bash
46
+ pnpm add -g ccbot
47
+ ccbot setup
48
+ ```
49
+
50
+ ### CΓ‘ch 2: npx (khΓ΄ng cαΊ§n cΓ i)
51
+
52
+ ```bash
53
+ npx ccbot setup
54
+ ```
55
+
56
+ ### CΓ‘ch 3: Clone repo (cho development)
57
+
58
+ ```bash
59
+ git clone https://github.com/palooza-kaida/ccbot.git
60
+ cd ccbot
61
+ pnpm install
62
+ pnpm setup
63
+ ```
64
+
65
+ Setup wizard sαΊ½ hΖ°α»›ng dαΊ«n tα»«ng bΖ°α»›c:
66
+
67
+ ```
68
+ β”Œ πŸ€– ccbot setup
69
+ β”‚
70
+ β—‡ Telegram Bot Token
71
+ β”‚ your-bot-token
72
+ β”‚
73
+ β—‡ Your Telegram User ID
74
+ β”‚ your-user-id
75
+ β”‚
76
+ β—† Config saved
77
+ β—† Hook installed β†’ ~/.claude/settings.json
78
+ β—† Chat ID registered
79
+ β”‚
80
+ β”” πŸŽ‰ Setup complete!
81
+ ```
82
+
83
+ <details>
84
+ <summary>ThiαΊΏt lαΊ­p thα»§ cΓ΄ng (khΓ΄ng dΓΉng wizard)</summary>
85
+
86
+ TαΊ‘o file `~/.ccbot/config.json`:
87
+
88
+ ```json
89
+ {
90
+ "telegram_bot_token": "123456:ABC-xxx",
91
+ "user_id": 123456789,
92
+ "hook_port": 9377
93
+ }
94
+ ```
95
+
96
+ Sau Δ‘Γ³ chαΊ‘y `ccbot setup` để cΓ i hook vΓ  Δ‘Δƒng kΓ½ chat ID.
97
+
98
+ </details>
99
+
100
+ ## Sα»­ dα»₯ng
101
+
102
+ ### Khởi Δ‘α»™ng bot
103
+
104
+ ```bash
105
+ # Global install
106
+ ccbot
107
+
108
+ # HoαΊ·c npx
109
+ npx ccbot
110
+
111
+ # HoαΊ·c local dev
112
+ pnpm dev
113
+ ```
114
+
115
+ Bot chαΊ‘y xong β†’ dΓΉng Claude Code bΓ¬nh thường β†’ notification tα»± Δ‘αΊΏn Telegram.
116
+
117
+ ### Telegram Commands
118
+
119
+ | Command | Chức năng |
120
+ |-----------|---------------------------------------------------|
121
+ | `/start` | Đăng kΓ½ lαΊ‘i chat (tα»± Δ‘α»™ng khi setup, Γ­t khi cαΊ§n) |
122
+ | `/ping` | Kiểm tra bot cΓ²n sα»‘ng khΓ΄ng |
123
+ | `/status` | Xem trαΊ‘ng thΓ‘i bot |
124
+
125
+ ### Notification mαΊ«u
126
+
127
+ ```
128
+ πŸ€– Claude Code Response
129
+ πŸ“‚ my-project | ⏱ 45s
130
+
131
+ Đã sα»­a bug authentication trong login.go. Thay Δ‘α»•i chΓ­nh:
132
+ - Fix missing error check ở dòng 42
133
+ - ThΓͺm input validation...
134
+
135
+ πŸ“‚ Changes:
136
+ ✏️ src/login.go
137
+ βž• src/validator.go
138
+ ❌ src/old_auth.go
139
+ ```
140
+
141
+ ## Gα»‘ cΓ i Δ‘αΊ·t
142
+
143
+ ```bash
144
+ ccbot uninstall
145
+ ```
146
+
147
+ ```
148
+ β”Œ πŸ—‘οΈ Uninstalling ccbot
149
+ β”‚
150
+ β—† Hook removed from ~/.claude/settings.json
151
+ β—† Removed ~/.ccbot/ (config, state, hooks)
152
+ β”‚
153
+ β”” ccbot uninstalled
154
+ ```
155
+
156
+ ## License
157
+
158
+ MIT
@@ -0,0 +1 @@
1
+ export declare function runHelp(): void;
@@ -0,0 +1,17 @@
1
+ import * as p from "@clack/prompts";
2
+ import { detectCliPrefix } from "../utils/install-detection.js";
3
+ export function runHelp() {
4
+ const prefix = detectCliPrefix();
5
+ p.intro("πŸ€– ccbot β€” Claude Code ↔ Telegram Notification Bot");
6
+ p.log.message([
7
+ `Usage: ${prefix} [command]`,
8
+ "",
9
+ "Commands:",
10
+ " (none) Start the bot",
11
+ " setup Interactive setup (config + hooks)",
12
+ " update Update ccbot to latest version",
13
+ " uninstall Remove all ccbot data and hooks",
14
+ " help Show this help message",
15
+ ].join("\n"));
16
+ p.outro("docs β†’ https://github.com/palooza-kaida/ccbot");
17
+ }
@@ -0,0 +1 @@
1
+ export declare function runSetup(): Promise<void>;
@@ -0,0 +1,103 @@
1
+ import * as p from "@clack/prompts";
2
+ import { mkdirSync, writeFileSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { ConfigManager } from "../config-manager.js";
6
+ import { HookInstaller } from "../hook/hook-installer.js";
7
+ import { detectCliPrefix } from "../utils/install-detection.js";
8
+ import { formatError } from "../utils/error-utils.js";
9
+ export async function runSetup() {
10
+ p.intro("πŸ€– ccbot setup");
11
+ let existing = null;
12
+ try {
13
+ existing = ConfigManager.load();
14
+ }
15
+ catch { }
16
+ const credentials = await promptCredentials(existing);
17
+ const config = buildConfig(credentials, existing);
18
+ saveConfig(config);
19
+ installHook(config);
20
+ registerChatId(config.user_id);
21
+ const startCommand = detectCliPrefix();
22
+ p.outro(`πŸŽ‰ Setup complete!\n\n Next steps:\n 1. Start bot: ${startCommand}\n 2. Use Claude Code normally β†’ notifications will arrive`);
23
+ }
24
+ async function promptCredentials(existing) {
25
+ const result = await p.group({
26
+ token: () => p.text({
27
+ message: "Telegram Bot Token",
28
+ placeholder: "Get from @BotFather β†’ /newbot",
29
+ initialValue: existing?.telegram_bot_token ?? "",
30
+ validate(value) {
31
+ if (!value || !value.trim())
32
+ return "Bot token is required";
33
+ if (!value.includes(":"))
34
+ return "Invalid format (expected: 123456:ABC-xxx)";
35
+ },
36
+ }),
37
+ userId: () => p.text({
38
+ message: "Your Telegram User ID",
39
+ placeholder: "Send /start to @userinfobot",
40
+ initialValue: existing?.user_id?.toString() ?? "",
41
+ validate(value) {
42
+ if (!value || !value.trim())
43
+ return "User ID is required";
44
+ if (isNaN(parseInt(value, 10)))
45
+ return "Must be a number";
46
+ },
47
+ }),
48
+ }, {
49
+ onCancel: () => {
50
+ p.cancel("Setup cancelled.");
51
+ process.exit(0);
52
+ },
53
+ });
54
+ return {
55
+ token: result.token.trim(),
56
+ userId: parseInt(result.userId.trim(), 10),
57
+ };
58
+ }
59
+ function buildConfig(credentials, existing) {
60
+ return {
61
+ telegram_bot_token: credentials.token,
62
+ user_id: credentials.userId,
63
+ hook_port: existing?.hook_port || 9377,
64
+ hook_secret: existing?.hook_secret || ConfigManager.generateSecret(),
65
+ };
66
+ }
67
+ function saveConfig(config) {
68
+ ConfigManager.save(config);
69
+ p.log.success("Config saved");
70
+ }
71
+ function installHook(config) {
72
+ try {
73
+ HookInstaller.install(config.hook_port, config.hook_secret);
74
+ p.log.success("Hook installed β†’ ~/.claude/settings.json");
75
+ }
76
+ catch (err) {
77
+ const msg = formatError(err);
78
+ if (msg.includes("already installed")) {
79
+ p.log.step("Hook already installed");
80
+ }
81
+ else {
82
+ p.log.error(`Hook installation failed: ${msg}`);
83
+ throw new Error(`install hook: ${msg}`);
84
+ }
85
+ }
86
+ }
87
+ function registerChatId(userId) {
88
+ const stateDir = join(homedir(), ".ccbot");
89
+ const stateFile = join(stateDir, "state.json");
90
+ let state = { chat_id: null };
91
+ try {
92
+ const data = readFileSync(stateFile, "utf-8");
93
+ state = JSON.parse(data);
94
+ }
95
+ catch { }
96
+ if (state.chat_id === userId) {
97
+ return;
98
+ }
99
+ state.chat_id = userId;
100
+ mkdirSync(stateDir, { recursive: true });
101
+ writeFileSync(stateFile, JSON.stringify(state, null, 2), { mode: 0o600 });
102
+ p.log.success("Chat ID registered");
103
+ }
@@ -0,0 +1 @@
1
+ export declare function runUninstall(): void;
@@ -0,0 +1,41 @@
1
+ import * as p from "@clack/prompts";
2
+ import { rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { HookInstaller } from "../hook/hook-installer.js";
6
+ import { detectInstallMethod } from "../utils/install-detection.js";
7
+ export function runUninstall() {
8
+ p.intro("πŸ—‘οΈ Uninstalling ccbot");
9
+ removeHook();
10
+ removeConfigDirectory();
11
+ printPostUninstallHint();
12
+ p.outro("ccbot uninstalled");
13
+ }
14
+ function removeHook() {
15
+ try {
16
+ HookInstaller.uninstall();
17
+ p.log.success("Hook removed from ~/.claude/settings.json");
18
+ }
19
+ catch {
20
+ p.log.warn("No hook found (already removed)");
21
+ }
22
+ }
23
+ function removeConfigDirectory() {
24
+ const ccbotDir = join(homedir(), ".ccbot");
25
+ try {
26
+ rmSync(ccbotDir, { recursive: true, force: true });
27
+ p.log.success("Removed ~/.ccbot/ (config, state, hooks)");
28
+ }
29
+ catch {
30
+ p.log.warn("~/.ccbot/ not found (already removed)");
31
+ }
32
+ }
33
+ function printPostUninstallHint() {
34
+ const method = detectInstallMethod();
35
+ if (method === "global") {
36
+ p.log.info("To also remove the package:\n pnpm remove -g ccbot");
37
+ }
38
+ else if (method === "git-clone") {
39
+ p.log.info("To also remove the source:\n rm -rf <ccbot-directory>");
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ export declare function runUpdate(): void;
@@ -0,0 +1,85 @@
1
+ import * as p from "@clack/prompts";
2
+ import { execSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { detectInstallMethod, getGitRepoRoot } from "../utils/install-detection.js";
6
+ export function runUpdate() {
7
+ const method = detectInstallMethod();
8
+ switch (method) {
9
+ case "npx":
10
+ p.intro("πŸ“¦ ccbot update");
11
+ p.log.step("Installed via npx β€” always uses latest version, no update needed.");
12
+ p.outro("Already up to date");
13
+ break;
14
+ case "global":
15
+ updateGlobal();
16
+ break;
17
+ case "git-clone":
18
+ updateGitClone();
19
+ break;
20
+ }
21
+ }
22
+ function detectGlobalPackageManager() {
23
+ const execPath = process.argv[1] ?? "";
24
+ if (execPath.includes("pnpm"))
25
+ return "pnpm";
26
+ if (execPath.includes("yarn"))
27
+ return "yarn";
28
+ if (execPath.includes("bun"))
29
+ return "bun";
30
+ return "npm";
31
+ }
32
+ function updateGlobal() {
33
+ const pm = detectGlobalPackageManager();
34
+ const pkg = "ccbot";
35
+ p.intro("πŸ“¦ ccbot update");
36
+ const s = p.spinner();
37
+ s.start(`Updating via ${pm}...`);
38
+ const cmd = pm === "yarn"
39
+ ? `yarn global add ${pkg}`
40
+ : `${pm} install -g ${pkg}@latest`;
41
+ try {
42
+ execSync(cmd, { stdio: "pipe" });
43
+ s.stop("Updated successfully");
44
+ p.outro("Update complete");
45
+ }
46
+ catch {
47
+ s.stop("Update failed");
48
+ p.log.error(`Try manually: ${cmd}`);
49
+ process.exit(1);
50
+ }
51
+ }
52
+ function updateGitClone() {
53
+ const scriptDir = dirname(process.argv[1] ?? "");
54
+ const repoRoot = getGitRepoRoot(scriptDir);
55
+ if (!repoRoot) {
56
+ p.log.error("Could not find git repo root.");
57
+ process.exit(1);
58
+ }
59
+ p.intro("πŸ“¦ ccbot update");
60
+ const s = p.spinner();
61
+ try {
62
+ s.start("Pulling latest changes...");
63
+ execSync("git pull", { cwd: repoRoot, stdio: "pipe" });
64
+ s.stop("Pulled latest changes");
65
+ const pm = existsSync(join(repoRoot, "pnpm-lock.yaml"))
66
+ ? "pnpm"
67
+ : existsSync(join(repoRoot, "yarn.lock"))
68
+ ? "yarn"
69
+ : existsSync(join(repoRoot, "bun.lockb"))
70
+ ? "bun"
71
+ : "npm";
72
+ s.start("Installing dependencies...");
73
+ execSync(`${pm} install`, { cwd: repoRoot, stdio: "pipe" });
74
+ s.stop("Dependencies installed");
75
+ s.start("Building...");
76
+ execSync(`${pm} run build`, { cwd: repoRoot, stdio: "pipe" });
77
+ s.stop("Build complete");
78
+ p.outro("Update complete");
79
+ }
80
+ catch {
81
+ s.stop("Update failed");
82
+ p.log.error("Try manually: git pull && npm install && npm run build");
83
+ process.exit(1);
84
+ }
85
+ }
@@ -0,0 +1,15 @@
1
+ export interface Config {
2
+ telegram_bot_token: string;
3
+ user_id: number;
4
+ hook_port: number;
5
+ hook_secret: string;
6
+ }
7
+ export declare class ConfigManager {
8
+ private static readonly CONFIG_DIR;
9
+ private static readonly CONFIG_FILE;
10
+ static load(): Config;
11
+ static save(cfg: Config): void;
12
+ static isOwner(cfg: Config, userId: number): boolean;
13
+ static generateSecret(): string;
14
+ private static validate;
15
+ }
@@ -0,0 +1,71 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { randomBytes } from "node:crypto";
5
+ export class ConfigManager {
6
+ static CONFIG_DIR = join(homedir(), ".ccbot");
7
+ static CONFIG_FILE = join(ConfigManager.CONFIG_DIR, "config.json");
8
+ static load() {
9
+ let data;
10
+ try {
11
+ data = readFileSync(ConfigManager.CONFIG_FILE, "utf-8");
12
+ }
13
+ catch (err) {
14
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
15
+ throw new Error("config not found β€” run 'ccbot setup' first");
16
+ }
17
+ throw new Error(`read config: ${err instanceof Error ? err.message : String(err)}`);
18
+ }
19
+ const raw = JSON.parse(data);
20
+ return ConfigManager.validate(raw);
21
+ }
22
+ static save(cfg) {
23
+ mkdirSync(ConfigManager.CONFIG_DIR, { recursive: true });
24
+ writeFileSync(ConfigManager.CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
25
+ }
26
+ static isOwner(cfg, userId) {
27
+ return cfg.user_id === userId;
28
+ }
29
+ static generateSecret() {
30
+ return randomBytes(32).toString("hex");
31
+ }
32
+ static validate(data) {
33
+ if (typeof data !== "object" || data === null) {
34
+ throw new Error("config must be a JSON object");
35
+ }
36
+ const obj = data;
37
+ if (typeof obj.telegram_bot_token !== "string" || !obj.telegram_bot_token.includes(":")) {
38
+ throw new Error("telegram_bot_token must be a string containing ':' β€” run 'ccbot setup'");
39
+ }
40
+ if (typeof obj.user_id !== "number" || !Number.isInteger(obj.user_id)) {
41
+ throw new Error("user_id must be an integer β€” run 'ccbot setup'");
42
+ }
43
+ let hookPort = 9377;
44
+ if (obj.hook_port !== undefined) {
45
+ if (typeof obj.hook_port !== "number" || !Number.isInteger(obj.hook_port) || obj.hook_port < 1 || obj.hook_port > 65535) {
46
+ throw new Error("hook_port must be an integer between 1 and 65535");
47
+ }
48
+ hookPort = obj.hook_port;
49
+ }
50
+ let hookSecret;
51
+ if (typeof obj.hook_secret === "string" && obj.hook_secret.length > 0) {
52
+ if (!/^[a-f0-9]+$/i.test(obj.hook_secret)) {
53
+ throw new Error("hook_secret must contain only hex characters (a-f, 0-9)");
54
+ }
55
+ hookSecret = obj.hook_secret;
56
+ }
57
+ else {
58
+ hookSecret = ConfigManager.generateSecret();
59
+ }
60
+ const cfg = {
61
+ telegram_bot_token: obj.telegram_bot_token,
62
+ user_id: obj.user_id,
63
+ hook_port: hookPort,
64
+ hook_secret: hookSecret,
65
+ };
66
+ if (typeof obj.hook_secret !== "string" || obj.hook_secret.length === 0) {
67
+ ConfigManager.save(cfg);
68
+ }
69
+ return cfg;
70
+ }
71
+ }
@@ -0,0 +1,9 @@
1
+ export type NotifyFunc = (text: string) => Promise<void>;
2
+ export declare class HookHandler {
3
+ private notify;
4
+ constructor(notify: NotifyFunc);
5
+ handleStopEvent(event: unknown): void;
6
+ private collectGitChanges;
7
+ private parseGitDiffOutput;
8
+ private parsePorcelainOutput;
9
+ }