ccbot 1.0.0 → 1.2.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/dist/commands/help.js +10 -9
- package/dist/commands/setup.js +41 -23
- package/dist/commands/uninstall.js +12 -10
- package/dist/commands/update.js +25 -23
- package/dist/config-manager.d.ts +2 -0
- package/dist/config-manager.js +14 -8
- package/dist/hook/hook-handler.d.ts +8 -3
- package/dist/hook/hook-handler.js +53 -18
- package/dist/hook/hook-installer.d.ts +1 -0
- package/dist/hook/hook-installer.js +19 -8
- package/dist/hook/hook-server.d.ts +2 -1
- package/dist/hook/hook-server.js +30 -6
- package/dist/hook/response-store.d.ts +8 -0
- package/dist/hook/response-store.js +18 -0
- package/dist/i18n/index.d.ts +9 -0
- package/dist/i18n/index.js +54 -0
- package/dist/i18n/locales/en.d.ts +2 -0
- package/dist/i18n/locales/en.js +121 -0
- package/dist/i18n/locales/vi.d.ts +2 -0
- package/dist/i18n/locales/vi.js +121 -0
- package/dist/i18n/locales/zh.d.ts +2 -0
- package/dist/i18n/locales/zh.js +121 -0
- package/dist/i18n/types.d.ts +121 -0
- package/dist/i18n/types.js +1 -0
- package/dist/index.js +38 -16
- package/dist/monitor/transcript-parser.d.ts +4 -0
- package/dist/monitor/transcript-parser.js +29 -3
- package/dist/telegram/bot.d.ts +8 -3
- package/dist/telegram/bot.js +69 -18
- package/dist/telegram/message-formatter.d.ts +7 -2
- package/dist/telegram/message-formatter.js +26 -23
- package/dist/telegram/message-sender.d.ts +1 -1
- package/dist/telegram/message-sender.js +37 -5
- package/dist/utils/constants.d.ts +23 -0
- package/dist/utils/constants.js +20 -0
- package/dist/utils/install-detection.d.ts +2 -1
- package/dist/utils/install-detection.js +6 -5
- package/dist/utils/log.d.ts +2 -0
- package/dist/utils/log.js +9 -0
- package/dist/utils/response-store.d.ts +27 -0
- package/dist/utils/response-store.js +84 -0
- package/dist/utils/tunnel.d.ts +7 -0
- package/dist/utils/tunnel.js +51 -0
- package/package.json +2 -1
package/dist/commands/help.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { detectCliPrefix } from "../utils/install-detection.js";
|
|
3
|
+
import { t } from "../i18n/index.js";
|
|
3
4
|
export function runHelp() {
|
|
4
5
|
const prefix = detectCliPrefix();
|
|
5
|
-
p.intro("
|
|
6
|
+
p.intro(t("help.intro"));
|
|
6
7
|
p.log.message([
|
|
7
|
-
|
|
8
|
+
t("help.usage", { prefix }),
|
|
8
9
|
"",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
10
|
+
t("help.commands"),
|
|
11
|
+
t("help.cmdNone"),
|
|
12
|
+
t("help.cmdSetup"),
|
|
13
|
+
t("help.cmdUpdate"),
|
|
14
|
+
t("help.cmdUninstall"),
|
|
15
|
+
t("help.cmdHelp"),
|
|
15
16
|
].join("\n"));
|
|
16
|
-
p.outro("docs
|
|
17
|
+
p.outro(t("help.docs"));
|
|
17
18
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -6,48 +6,66 @@ import { ConfigManager } from "../config-manager.js";
|
|
|
6
6
|
import { HookInstaller } from "../hook/hook-installer.js";
|
|
7
7
|
import { detectCliPrefix } from "../utils/install-detection.js";
|
|
8
8
|
import { formatError } from "../utils/error-utils.js";
|
|
9
|
+
import { t, setLocale, SUPPORTED_LOCALES, LOCALE_LABELS } from "../i18n/index.js";
|
|
9
10
|
export async function runSetup() {
|
|
10
|
-
p.intro("
|
|
11
|
+
p.intro(t("setup.intro"));
|
|
11
12
|
let existing = null;
|
|
12
13
|
try {
|
|
13
14
|
existing = ConfigManager.load();
|
|
14
15
|
}
|
|
15
16
|
catch { }
|
|
17
|
+
const locale = await promptLanguage(existing);
|
|
18
|
+
setLocale(locale);
|
|
16
19
|
const credentials = await promptCredentials(existing);
|
|
17
|
-
const config = buildConfig(credentials, existing);
|
|
20
|
+
const config = buildConfig(credentials, existing, locale);
|
|
18
21
|
saveConfig(config);
|
|
19
22
|
installHook(config);
|
|
20
23
|
registerChatId(config.user_id);
|
|
21
24
|
const startCommand = detectCliPrefix();
|
|
22
|
-
p.outro(
|
|
25
|
+
p.outro(t("setup.complete", { command: startCommand }));
|
|
26
|
+
}
|
|
27
|
+
async function promptLanguage(existing) {
|
|
28
|
+
const result = await p.select({
|
|
29
|
+
message: t("setup.languageMessage"),
|
|
30
|
+
initialValue: existing?.locale ?? "en",
|
|
31
|
+
options: SUPPORTED_LOCALES.map((loc) => ({
|
|
32
|
+
value: loc,
|
|
33
|
+
label: LOCALE_LABELS[loc],
|
|
34
|
+
})),
|
|
35
|
+
});
|
|
36
|
+
if (p.isCancel(result)) {
|
|
37
|
+
p.cancel(t("setup.cancelled"));
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
23
41
|
}
|
|
24
42
|
async function promptCredentials(existing) {
|
|
25
43
|
const result = await p.group({
|
|
26
44
|
token: () => p.text({
|
|
27
|
-
message: "
|
|
28
|
-
placeholder: "
|
|
45
|
+
message: t("setup.tokenMessage"),
|
|
46
|
+
placeholder: t("setup.tokenPlaceholder"),
|
|
29
47
|
initialValue: existing?.telegram_bot_token ?? "",
|
|
30
48
|
validate(value) {
|
|
31
49
|
if (!value || !value.trim())
|
|
32
|
-
return "
|
|
50
|
+
return t("setup.tokenRequired");
|
|
33
51
|
if (!value.includes(":"))
|
|
34
|
-
return "
|
|
52
|
+
return t("setup.tokenInvalidFormat");
|
|
35
53
|
},
|
|
36
54
|
}),
|
|
37
55
|
userId: () => p.text({
|
|
38
|
-
message: "
|
|
39
|
-
placeholder: "
|
|
56
|
+
message: t("setup.userIdMessage"),
|
|
57
|
+
placeholder: t("setup.userIdPlaceholder"),
|
|
40
58
|
initialValue: existing?.user_id?.toString() ?? "",
|
|
41
59
|
validate(value) {
|
|
42
60
|
if (!value || !value.trim())
|
|
43
|
-
return "
|
|
61
|
+
return t("setup.userIdRequired");
|
|
44
62
|
if (isNaN(parseInt(value, 10)))
|
|
45
|
-
return "
|
|
63
|
+
return t("setup.userIdMustBeNumber");
|
|
46
64
|
},
|
|
47
65
|
}),
|
|
48
66
|
}, {
|
|
49
67
|
onCancel: () => {
|
|
50
|
-
p.cancel("
|
|
68
|
+
p.cancel(t("setup.cancelled"));
|
|
51
69
|
process.exit(0);
|
|
52
70
|
},
|
|
53
71
|
});
|
|
@@ -56,32 +74,32 @@ async function promptCredentials(existing) {
|
|
|
56
74
|
userId: parseInt(result.userId.trim(), 10),
|
|
57
75
|
};
|
|
58
76
|
}
|
|
59
|
-
function buildConfig(credentials, existing) {
|
|
77
|
+
function buildConfig(credentials, existing, locale) {
|
|
60
78
|
return {
|
|
61
79
|
telegram_bot_token: credentials.token,
|
|
62
80
|
user_id: credentials.userId,
|
|
63
81
|
hook_port: existing?.hook_port || 9377,
|
|
64
82
|
hook_secret: existing?.hook_secret || ConfigManager.generateSecret(),
|
|
83
|
+
locale,
|
|
65
84
|
};
|
|
66
85
|
}
|
|
67
86
|
function saveConfig(config) {
|
|
68
87
|
ConfigManager.save(config);
|
|
69
|
-
p.log.success("
|
|
88
|
+
p.log.success(t("setup.configSaved"));
|
|
70
89
|
}
|
|
71
90
|
function installHook(config) {
|
|
91
|
+
if (HookInstaller.isInstalled()) {
|
|
92
|
+
p.log.step(t("setup.hookAlreadyInstalled"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
72
95
|
try {
|
|
73
96
|
HookInstaller.install(config.hook_port, config.hook_secret);
|
|
74
|
-
p.log.success("
|
|
97
|
+
p.log.success(t("setup.hookInstalled"));
|
|
75
98
|
}
|
|
76
99
|
catch (err) {
|
|
77
100
|
const msg = formatError(err);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
p.log.error(`Hook installation failed: ${msg}`);
|
|
83
|
-
throw new Error(`install hook: ${msg}`);
|
|
84
|
-
}
|
|
101
|
+
p.log.error(t("setup.hookFailed", { error: msg }));
|
|
102
|
+
throw new Error(`install hook: ${msg}`);
|
|
85
103
|
}
|
|
86
104
|
}
|
|
87
105
|
function registerChatId(userId) {
|
|
@@ -99,5 +117,5 @@ function registerChatId(userId) {
|
|
|
99
117
|
state.chat_id = userId;
|
|
100
118
|
mkdirSync(stateDir, { recursive: true });
|
|
101
119
|
writeFileSync(stateFile, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
102
|
-
p.log.success("
|
|
120
|
+
p.log.success(t("setup.chatIdRegistered"));
|
|
103
121
|
}
|
|
@@ -4,38 +4,40 @@ import { join } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { HookInstaller } from "../hook/hook-installer.js";
|
|
6
6
|
import { detectInstallMethod } from "../utils/install-detection.js";
|
|
7
|
+
import { InstallMethod } from "../utils/constants.js";
|
|
8
|
+
import { t } from "../i18n/index.js";
|
|
7
9
|
export function runUninstall() {
|
|
8
|
-
p.intro("
|
|
10
|
+
p.intro(t("uninstall.intro"));
|
|
9
11
|
removeHook();
|
|
10
12
|
removeConfigDirectory();
|
|
11
13
|
printPostUninstallHint();
|
|
12
|
-
p.outro("
|
|
14
|
+
p.outro(t("uninstall.done"));
|
|
13
15
|
}
|
|
14
16
|
function removeHook() {
|
|
15
17
|
try {
|
|
16
18
|
HookInstaller.uninstall();
|
|
17
|
-
p.log.success("
|
|
19
|
+
p.log.success(t("uninstall.hookRemoved"));
|
|
18
20
|
}
|
|
19
21
|
catch {
|
|
20
|
-
p.log.warn(
|
|
22
|
+
p.log.warn(t("uninstall.hookNotFound"));
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
function removeConfigDirectory() {
|
|
24
26
|
const ccbotDir = join(homedir(), ".ccbot");
|
|
25
27
|
try {
|
|
26
28
|
rmSync(ccbotDir, { recursive: true, force: true });
|
|
27
|
-
p.log.success(
|
|
29
|
+
p.log.success(t("uninstall.configRemoved"));
|
|
28
30
|
}
|
|
29
31
|
catch {
|
|
30
|
-
p.log.warn(
|
|
32
|
+
p.log.warn(t("uninstall.configNotFound"));
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
function printPostUninstallHint() {
|
|
34
36
|
const method = detectInstallMethod();
|
|
35
|
-
if (method ===
|
|
36
|
-
p.log.info("
|
|
37
|
+
if (method === InstallMethod.Global) {
|
|
38
|
+
p.log.info(t("uninstall.removeGlobal"));
|
|
37
39
|
}
|
|
38
|
-
else if (method ===
|
|
39
|
-
p.log.info("
|
|
40
|
+
else if (method === InstallMethod.GitClone) {
|
|
41
|
+
p.log.info(t("uninstall.removeGitClone"));
|
|
40
42
|
}
|
|
41
43
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -3,18 +3,20 @@ import { execSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { detectInstallMethod, getGitRepoRoot } from "../utils/install-detection.js";
|
|
6
|
+
import { InstallMethod } from "../utils/constants.js";
|
|
7
|
+
import { t } from "../i18n/index.js";
|
|
6
8
|
export function runUpdate() {
|
|
7
9
|
const method = detectInstallMethod();
|
|
8
10
|
switch (method) {
|
|
9
|
-
case
|
|
10
|
-
p.intro("
|
|
11
|
-
p.log.step("
|
|
12
|
-
p.outro("
|
|
11
|
+
case InstallMethod.Npx:
|
|
12
|
+
p.intro(t("update.intro"));
|
|
13
|
+
p.log.step(t("update.npxAlreadyLatest"));
|
|
14
|
+
p.outro(t("update.npxDone"));
|
|
13
15
|
break;
|
|
14
|
-
case
|
|
16
|
+
case InstallMethod.Global:
|
|
15
17
|
updateGlobal();
|
|
16
18
|
break;
|
|
17
|
-
case
|
|
19
|
+
case InstallMethod.GitClone:
|
|
18
20
|
updateGitClone();
|
|
19
21
|
break;
|
|
20
22
|
}
|
|
@@ -32,20 +34,20 @@ function detectGlobalPackageManager() {
|
|
|
32
34
|
function updateGlobal() {
|
|
33
35
|
const pm = detectGlobalPackageManager();
|
|
34
36
|
const pkg = "ccbot";
|
|
35
|
-
p.intro("
|
|
37
|
+
p.intro(t("update.intro"));
|
|
36
38
|
const s = p.spinner();
|
|
37
|
-
s.start(
|
|
39
|
+
s.start(t("update.updating", { pm }));
|
|
38
40
|
const cmd = pm === "yarn"
|
|
39
41
|
? `yarn global add ${pkg}`
|
|
40
42
|
: `${pm} install -g ${pkg}@latest`;
|
|
41
43
|
try {
|
|
42
44
|
execSync(cmd, { stdio: "pipe" });
|
|
43
|
-
s.stop("
|
|
44
|
-
p.outro("
|
|
45
|
+
s.stop(t("update.updateSuccess"));
|
|
46
|
+
p.outro(t("update.updateComplete"));
|
|
45
47
|
}
|
|
46
48
|
catch {
|
|
47
|
-
s.stop("
|
|
48
|
-
p.log.error(
|
|
49
|
+
s.stop(t("update.updateFailed"));
|
|
50
|
+
p.log.error(t("update.updateManualGlobal", { cmd }));
|
|
49
51
|
process.exit(1);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -53,15 +55,15 @@ function updateGitClone() {
|
|
|
53
55
|
const scriptDir = dirname(process.argv[1] ?? "");
|
|
54
56
|
const repoRoot = getGitRepoRoot(scriptDir);
|
|
55
57
|
if (!repoRoot) {
|
|
56
|
-
p.log.error("
|
|
58
|
+
p.log.error(t("update.gitRepoNotFound"));
|
|
57
59
|
process.exit(1);
|
|
58
60
|
}
|
|
59
|
-
p.intro("
|
|
61
|
+
p.intro(t("update.intro"));
|
|
60
62
|
const s = p.spinner();
|
|
61
63
|
try {
|
|
62
|
-
s.start("
|
|
64
|
+
s.start(t("update.pulling"));
|
|
63
65
|
execSync("git pull", { cwd: repoRoot, stdio: "pipe" });
|
|
64
|
-
s.stop("
|
|
66
|
+
s.stop(t("update.pulled"));
|
|
65
67
|
const pm = existsSync(join(repoRoot, "pnpm-lock.yaml"))
|
|
66
68
|
? "pnpm"
|
|
67
69
|
: existsSync(join(repoRoot, "yarn.lock"))
|
|
@@ -69,17 +71,17 @@ function updateGitClone() {
|
|
|
69
71
|
: existsSync(join(repoRoot, "bun.lockb"))
|
|
70
72
|
? "bun"
|
|
71
73
|
: "npm";
|
|
72
|
-
s.start("
|
|
74
|
+
s.start(t("update.installingDeps"));
|
|
73
75
|
execSync(`${pm} install`, { cwd: repoRoot, stdio: "pipe" });
|
|
74
|
-
s.stop("
|
|
75
|
-
s.start("
|
|
76
|
+
s.stop(t("update.depsInstalled"));
|
|
77
|
+
s.start(t("update.building"));
|
|
76
78
|
execSync(`${pm} run build`, { cwd: repoRoot, stdio: "pipe" });
|
|
77
|
-
s.stop("
|
|
78
|
-
p.outro("
|
|
79
|
+
s.stop(t("update.buildComplete"));
|
|
80
|
+
p.outro(t("update.updateComplete"));
|
|
79
81
|
}
|
|
80
82
|
catch {
|
|
81
|
-
s.stop("
|
|
82
|
-
p.log.error("
|
|
83
|
+
s.stop(t("update.updateFailed"));
|
|
84
|
+
p.log.error(t("update.updateManualGit"));
|
|
83
85
|
process.exit(1);
|
|
84
86
|
}
|
|
85
87
|
}
|
package/dist/config-manager.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type Locale } from "./i18n/index.js";
|
|
1
2
|
export interface Config {
|
|
2
3
|
telegram_bot_token: string;
|
|
3
4
|
user_id: number;
|
|
4
5
|
hook_port: number;
|
|
5
6
|
hook_secret: string;
|
|
7
|
+
locale: Locale;
|
|
6
8
|
}
|
|
7
9
|
export declare class ConfigManager {
|
|
8
10
|
private static readonly CONFIG_DIR;
|
package/dist/config-manager.js
CHANGED
|
@@ -2,6 +2,8 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { isValidLocale, setLocale } from "./i18n/index.js";
|
|
6
|
+
import { t } from "./i18n/index.js";
|
|
5
7
|
export class ConfigManager {
|
|
6
8
|
static CONFIG_DIR = join(homedir(), ".ccbot");
|
|
7
9
|
static CONFIG_FILE = join(ConfigManager.CONFIG_DIR, "config.json");
|
|
@@ -12,12 +14,14 @@ export class ConfigManager {
|
|
|
12
14
|
}
|
|
13
15
|
catch (err) {
|
|
14
16
|
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
15
|
-
throw new Error("config
|
|
17
|
+
throw new Error(t("config.notFound"));
|
|
16
18
|
}
|
|
17
|
-
throw new Error(
|
|
19
|
+
throw new Error(t("config.readError", { error: err instanceof Error ? err.message : String(err) }));
|
|
18
20
|
}
|
|
19
21
|
const raw = JSON.parse(data);
|
|
20
|
-
|
|
22
|
+
const cfg = ConfigManager.validate(raw);
|
|
23
|
+
setLocale(cfg.locale);
|
|
24
|
+
return cfg;
|
|
21
25
|
}
|
|
22
26
|
static save(cfg) {
|
|
23
27
|
mkdirSync(ConfigManager.CONFIG_DIR, { recursive: true });
|
|
@@ -31,37 +35,39 @@ export class ConfigManager {
|
|
|
31
35
|
}
|
|
32
36
|
static validate(data) {
|
|
33
37
|
if (typeof data !== "object" || data === null) {
|
|
34
|
-
throw new Error("config
|
|
38
|
+
throw new Error(t("config.mustBeObject"));
|
|
35
39
|
}
|
|
36
40
|
const obj = data;
|
|
37
41
|
if (typeof obj.telegram_bot_token !== "string" || !obj.telegram_bot_token.includes(":")) {
|
|
38
|
-
throw new Error("
|
|
42
|
+
throw new Error(t("config.invalidToken"));
|
|
39
43
|
}
|
|
40
44
|
if (typeof obj.user_id !== "number" || !Number.isInteger(obj.user_id)) {
|
|
41
|
-
throw new Error("
|
|
45
|
+
throw new Error(t("config.invalidUserId"));
|
|
42
46
|
}
|
|
43
47
|
let hookPort = 9377;
|
|
44
48
|
if (obj.hook_port !== undefined) {
|
|
45
49
|
if (typeof obj.hook_port !== "number" || !Number.isInteger(obj.hook_port) || obj.hook_port < 1 || obj.hook_port > 65535) {
|
|
46
|
-
throw new Error("
|
|
50
|
+
throw new Error(t("config.invalidPort"));
|
|
47
51
|
}
|
|
48
52
|
hookPort = obj.hook_port;
|
|
49
53
|
}
|
|
50
54
|
let hookSecret;
|
|
51
55
|
if (typeof obj.hook_secret === "string" && obj.hook_secret.length > 0) {
|
|
52
56
|
if (!/^[a-f0-9]+$/i.test(obj.hook_secret)) {
|
|
53
|
-
throw new Error(
|
|
57
|
+
throw new Error(t("config.invalidSecret"));
|
|
54
58
|
}
|
|
55
59
|
hookSecret = obj.hook_secret;
|
|
56
60
|
}
|
|
57
61
|
else {
|
|
58
62
|
hookSecret = ConfigManager.generateSecret();
|
|
59
63
|
}
|
|
64
|
+
const locale = isValidLocale(obj.locale) ? obj.locale : "en";
|
|
60
65
|
const cfg = {
|
|
61
66
|
telegram_bot_token: obj.telegram_bot_token,
|
|
62
67
|
user_id: obj.user_id,
|
|
63
68
|
hook_port: hookPort,
|
|
64
69
|
hook_secret: hookSecret,
|
|
70
|
+
locale,
|
|
65
71
|
};
|
|
66
72
|
if (typeof obj.hook_secret !== "string" || obj.hook_secret.length === 0) {
|
|
67
73
|
ConfigManager.save(cfg);
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import type { TunnelManager } from "../utils/tunnel.js";
|
|
2
|
+
type NotifyFunc = (text: string, responseUrl?: string) => Promise<void>;
|
|
2
3
|
export declare class HookHandler {
|
|
3
4
|
private notify;
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
private hookPort;
|
|
6
|
+
private tunnelManager;
|
|
7
|
+
constructor(notify: NotifyFunc, hookPort: number, tunnelManager: TunnelManager);
|
|
8
|
+
handleStopEvent(event: unknown): Promise<void>;
|
|
9
|
+
private buildResponseUrl;
|
|
6
10
|
private collectGitChanges;
|
|
7
11
|
private parseGitDiffOutput;
|
|
8
12
|
private parsePorcelainOutput;
|
|
9
13
|
}
|
|
14
|
+
export {};
|
|
@@ -2,6 +2,10 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { parseTranscript } from "../monitor/transcript-parser.js";
|
|
3
3
|
import { formatNotification, extractProjectName, } from "../telegram/message-formatter.js";
|
|
4
4
|
import { formatError } from "../utils/error-utils.js";
|
|
5
|
+
import { GitChangeStatus, MINI_APP_BASE_URL } from "../utils/constants.js";
|
|
6
|
+
import { t } from "../i18n/index.js";
|
|
7
|
+
import { responseStore } from "../utils/response-store.js";
|
|
8
|
+
import { log } from "../utils/log.js";
|
|
5
9
|
const GIT_TIMEOUT_MS = 10_000;
|
|
6
10
|
function isValidStopEvent(data) {
|
|
7
11
|
if (typeof data !== "object" || data === null)
|
|
@@ -13,36 +17,67 @@ function isValidStopEvent(data) {
|
|
|
13
17
|
}
|
|
14
18
|
export class HookHandler {
|
|
15
19
|
notify;
|
|
16
|
-
|
|
20
|
+
hookPort;
|
|
21
|
+
tunnelManager;
|
|
22
|
+
constructor(notify, hookPort, tunnelManager) {
|
|
17
23
|
this.notify = notify;
|
|
24
|
+
this.hookPort = hookPort;
|
|
25
|
+
this.tunnelManager = tunnelManager;
|
|
18
26
|
}
|
|
19
|
-
handleStopEvent(event) {
|
|
27
|
+
async handleStopEvent(event) {
|
|
20
28
|
if (!isValidStopEvent(event)) {
|
|
21
|
-
|
|
29
|
+
log(t("hook.invalidPayload"));
|
|
22
30
|
return;
|
|
23
31
|
}
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
log(t("hook.stopEventReceived", { sessionId: event.session_id, cwd: event.cwd }));
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
34
|
+
let summary = { lastAssistantMessage: "", durationMs: 0, totalCostUSD: 0, inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
26
35
|
try {
|
|
27
36
|
summary = parseTranscript(event.transcript_path);
|
|
28
37
|
}
|
|
29
38
|
catch (err) {
|
|
30
|
-
|
|
39
|
+
log(t("hook.transcriptFailed", { error: formatError(err) }));
|
|
31
40
|
}
|
|
32
41
|
const gitChanges = this.collectGitChanges(event.cwd);
|
|
33
42
|
let durationMs = summary.durationMs;
|
|
34
43
|
if (durationMs === 0 && summary.lastAssistantMessage) {
|
|
35
44
|
durationMs = 1000;
|
|
36
45
|
}
|
|
37
|
-
const
|
|
46
|
+
const data = {
|
|
38
47
|
projectName: extractProjectName(event.cwd),
|
|
39
48
|
responseSummary: summary.lastAssistantMessage,
|
|
40
49
|
durationMs,
|
|
41
50
|
gitChanges,
|
|
51
|
+
inputTokens: summary.inputTokens,
|
|
52
|
+
outputTokens: summary.outputTokens,
|
|
53
|
+
cacheCreationTokens: summary.cacheCreationTokens,
|
|
54
|
+
cacheReadTokens: summary.cacheReadTokens,
|
|
55
|
+
};
|
|
56
|
+
const notification = formatNotification(data);
|
|
57
|
+
const responseUrl = this.buildResponseUrl(data);
|
|
58
|
+
this.notify(notification, responseUrl).catch((err) => {
|
|
59
|
+
log(t("hook.notificationFailed", { error: formatError(err) }));
|
|
42
60
|
});
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
}
|
|
62
|
+
buildResponseUrl(data) {
|
|
63
|
+
const id = responseStore.save({
|
|
64
|
+
projectName: data.projectName,
|
|
65
|
+
responseSummary: data.responseSummary,
|
|
66
|
+
durationMs: data.durationMs,
|
|
67
|
+
gitChanges: data.gitChanges,
|
|
68
|
+
inputTokens: data.inputTokens,
|
|
69
|
+
outputTokens: data.outputTokens,
|
|
70
|
+
cacheCreationTokens: data.cacheCreationTokens,
|
|
71
|
+
cacheReadTokens: data.cacheReadTokens,
|
|
72
|
+
});
|
|
73
|
+
const apiBase = this.tunnelManager.getPublicUrl() || `http://localhost:${this.hookPort}`;
|
|
74
|
+
const params = new URLSearchParams({
|
|
75
|
+
id,
|
|
76
|
+
api: apiBase,
|
|
77
|
+
p: data.projectName,
|
|
78
|
+
d: String(data.durationMs),
|
|
45
79
|
});
|
|
80
|
+
return `${MINI_APP_BASE_URL}/response.html?${params.toString()}`;
|
|
46
81
|
}
|
|
47
82
|
collectGitChanges(cwd) {
|
|
48
83
|
try {
|
|
@@ -60,7 +95,7 @@ export class HookHandler {
|
|
|
60
95
|
});
|
|
61
96
|
for (const file of untrackedOutput.trim().split("\n")) {
|
|
62
97
|
if (file)
|
|
63
|
-
changes.push({ file, status:
|
|
98
|
+
changes.push({ file, status: GitChangeStatus.Added });
|
|
64
99
|
}
|
|
65
100
|
}
|
|
66
101
|
catch { }
|
|
@@ -88,13 +123,13 @@ export class HookHandler {
|
|
|
88
123
|
const parts = line.split("\t");
|
|
89
124
|
if (parts.length < 2)
|
|
90
125
|
continue;
|
|
91
|
-
let status =
|
|
126
|
+
let status = GitChangeStatus.Modified;
|
|
92
127
|
if (parts[0].startsWith("A"))
|
|
93
|
-
status =
|
|
128
|
+
status = GitChangeStatus.Added;
|
|
94
129
|
else if (parts[0].startsWith("D"))
|
|
95
|
-
status =
|
|
130
|
+
status = GitChangeStatus.Deleted;
|
|
96
131
|
else if (parts[0].startsWith("R"))
|
|
97
|
-
status =
|
|
132
|
+
status = GitChangeStatus.Renamed;
|
|
98
133
|
changes.push({ file: parts[1], status });
|
|
99
134
|
}
|
|
100
135
|
return changes;
|
|
@@ -106,17 +141,17 @@ export class HookHandler {
|
|
|
106
141
|
continue;
|
|
107
142
|
const statusCode = line.slice(0, 2).trim();
|
|
108
143
|
const file = line.slice(3).trim();
|
|
109
|
-
let status =
|
|
144
|
+
let status = GitChangeStatus.Modified;
|
|
110
145
|
switch (statusCode) {
|
|
111
146
|
case "??":
|
|
112
147
|
case "A":
|
|
113
|
-
status =
|
|
148
|
+
status = GitChangeStatus.Added;
|
|
114
149
|
break;
|
|
115
150
|
case "D":
|
|
116
|
-
status =
|
|
151
|
+
status = GitChangeStatus.Deleted;
|
|
117
152
|
break;
|
|
118
153
|
case "R":
|
|
119
|
-
status =
|
|
154
|
+
status = GitChangeStatus.Renamed;
|
|
120
155
|
break;
|
|
121
156
|
}
|
|
122
157
|
changes.push({ file, status });
|
|
@@ -2,6 +2,7 @@ export declare class HookInstaller {
|
|
|
2
2
|
private static readonly HOOKS_DIR;
|
|
3
3
|
private static readonly SCRIPT_PATH;
|
|
4
4
|
private static readonly SETTINGS_PATH;
|
|
5
|
+
static isInstalled(): boolean;
|
|
5
6
|
static install(hookPort: number, hookSecret: string): void;
|
|
6
7
|
static uninstall(): void;
|
|
7
8
|
private static installScript;
|
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, rmdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { t } from "../i18n/index.js";
|
|
4
5
|
export class HookInstaller {
|
|
5
6
|
static HOOKS_DIR = join(homedir(), ".ccbot", "hooks");
|
|
6
7
|
static SCRIPT_PATH = join(HookInstaller.HOOKS_DIR, "stop-notify.sh");
|
|
7
8
|
static SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
9
|
+
static isInstalled() {
|
|
10
|
+
try {
|
|
11
|
+
const settings = HookInstaller.readSettings();
|
|
12
|
+
const hooks = (settings.hooks ?? {});
|
|
13
|
+
const existingStop = (hooks.Stop ?? []);
|
|
14
|
+
return existingStop.some((entry) => {
|
|
15
|
+
const entryHooks = entry.hooks;
|
|
16
|
+
return entryHooks?.some((h) => typeof h.command === "string" && h.command.includes("ccbot"));
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
static install(hookPort, hookSecret) {
|
|
9
24
|
if (!Number.isInteger(hookPort) || hookPort < 1 || hookPort > 65535) {
|
|
10
|
-
throw new Error(
|
|
25
|
+
throw new Error(t("config.invalidHookPort", { port: hookPort }));
|
|
11
26
|
}
|
|
12
27
|
const settings = HookInstaller.readSettings();
|
|
13
28
|
const hooks = (settings.hooks ?? {});
|
|
14
29
|
const existingStop = (hooks.Stop ?? []);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return entryHooks?.some((h) => typeof h.command === "string" && h.command.includes("ccbot"));
|
|
18
|
-
});
|
|
19
|
-
if (alreadyInstalled) {
|
|
20
|
-
throw new Error("ccbot hook already installed");
|
|
30
|
+
if (HookInstaller.isInstalled()) {
|
|
31
|
+
throw new Error(t("config.hookAlreadyInstalled"));
|
|
21
32
|
}
|
|
22
33
|
existingStop.push({
|
|
23
34
|
hooks: [{ type: "command", command: HookInstaller.SCRIPT_PATH, timeout: 10 }],
|
|
@@ -91,7 +102,7 @@ curl -s -X POST http://localhost:${hookPort}/hook/stop \\
|
|
|
91
102
|
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
92
103
|
return {};
|
|
93
104
|
}
|
|
94
|
-
throw new Error(
|
|
105
|
+
throw new Error(t("config.readSettingsError", { error: err instanceof Error ? err.message : String(err) }));
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
}
|
|
@@ -5,7 +5,8 @@ export declare class HookServer {
|
|
|
5
5
|
private port;
|
|
6
6
|
private secret;
|
|
7
7
|
private handler;
|
|
8
|
-
constructor(port: number, secret: string
|
|
8
|
+
constructor(port: number, secret: string);
|
|
9
|
+
setHandler(handler: HookHandler): void;
|
|
9
10
|
start(): void;
|
|
10
11
|
stop(): Promise<void>;
|
|
11
12
|
private createApp;
|