@gonzih/cc-tg 0.2.21 → 0.3.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/README.md +27 -6
- package/package.json +1 -1
- package/dist/bot.d.ts +0 -50
- package/dist/bot.js +0 -1100
- package/dist/claude.d.ts +0 -54
- package/dist/claude.js +0 -208
- package/dist/cron.d.ts +0 -33
- package/dist/cron.js +0 -127
- package/dist/index.d.ts +0 -16
- package/dist/index.js +0 -94
- package/dist/voice.d.ts +0 -13
- package/dist/voice.js +0 -124
package/dist/claude.d.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code subprocess wrapper.
|
|
3
|
-
* Mirrors ce_ce's mechanism: spawn `claude` CLI with stream-json I/O,
|
|
4
|
-
* pipe prompts in, parse streaming JSON messages out.
|
|
5
|
-
*/
|
|
6
|
-
import { EventEmitter } from "events";
|
|
7
|
-
export type MessageType = "system" | "assistant" | "user" | "result";
|
|
8
|
-
export interface ClaudeMessage {
|
|
9
|
-
type: MessageType;
|
|
10
|
-
session_id?: string;
|
|
11
|
-
uuid?: string;
|
|
12
|
-
payload: Record<string, unknown>;
|
|
13
|
-
raw: Record<string, unknown>;
|
|
14
|
-
}
|
|
15
|
-
export interface ClaudeOptions {
|
|
16
|
-
cwd?: string;
|
|
17
|
-
systemPrompt?: string;
|
|
18
|
-
/** OAuth token (sk-ant-oat01-...) or API key (sk-ant-api03-...) */
|
|
19
|
-
token?: string;
|
|
20
|
-
}
|
|
21
|
-
export interface UsageEvent {
|
|
22
|
-
inputTokens: number;
|
|
23
|
-
outputTokens: number;
|
|
24
|
-
cacheReadTokens: number;
|
|
25
|
-
cacheWriteTokens: number;
|
|
26
|
-
}
|
|
27
|
-
export declare interface ClaudeProcess {
|
|
28
|
-
on(event: "message", listener: (msg: ClaudeMessage) => void): this;
|
|
29
|
-
on(event: "usage", listener: (usage: UsageEvent) => void): this;
|
|
30
|
-
on(event: "error", listener: (err: Error) => void): this;
|
|
31
|
-
on(event: "exit", listener: (code: number | null) => void): this;
|
|
32
|
-
on(event: "stderr", listener: (data: string) => void): this;
|
|
33
|
-
}
|
|
34
|
-
export declare class ClaudeProcess extends EventEmitter {
|
|
35
|
-
private proc;
|
|
36
|
-
private buffer;
|
|
37
|
-
private _exited;
|
|
38
|
-
constructor(opts?: ClaudeOptions);
|
|
39
|
-
sendPrompt(text: string): void;
|
|
40
|
-
/**
|
|
41
|
-
* Send an image (with optional text caption) to Claude via stream-json content blocks.
|
|
42
|
-
* mediaType: image/jpeg | image/png | image/gif | image/webp
|
|
43
|
-
*/
|
|
44
|
-
sendImage(base64Data: string, mediaType: string, caption?: string): void;
|
|
45
|
-
kill(): void;
|
|
46
|
-
get exited(): boolean;
|
|
47
|
-
private drainBuffer;
|
|
48
|
-
private parseMessage;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Extract the text content from an assistant message payload.
|
|
52
|
-
* Handles both simple string content and content-block arrays.
|
|
53
|
-
*/
|
|
54
|
-
export declare function extractText(msg: ClaudeMessage): string;
|
package/dist/claude.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code subprocess wrapper.
|
|
3
|
-
* Mirrors ce_ce's mechanism: spawn `claude` CLI with stream-json I/O,
|
|
4
|
-
* pipe prompts in, parse streaming JSON messages out.
|
|
5
|
-
*/
|
|
6
|
-
import { spawn } from "child_process";
|
|
7
|
-
import { EventEmitter } from "events";
|
|
8
|
-
import { existsSync } from "fs";
|
|
9
|
-
export class ClaudeProcess extends EventEmitter {
|
|
10
|
-
proc;
|
|
11
|
-
buffer = "";
|
|
12
|
-
_exited = false;
|
|
13
|
-
constructor(opts = {}) {
|
|
14
|
-
super();
|
|
15
|
-
const args = [
|
|
16
|
-
"--continue",
|
|
17
|
-
"--output-format", "stream-json",
|
|
18
|
-
"--input-format", "stream-json",
|
|
19
|
-
"--print",
|
|
20
|
-
"--verbose",
|
|
21
|
-
"--dangerously-skip-permissions",
|
|
22
|
-
];
|
|
23
|
-
if (opts.systemPrompt) {
|
|
24
|
-
args.push("--system-prompt", opts.systemPrompt);
|
|
25
|
-
}
|
|
26
|
-
const env = { ...process.env };
|
|
27
|
-
if (opts.token) {
|
|
28
|
-
// API keys start with sk-ant-api — set ANTHROPIC_API_KEY only
|
|
29
|
-
// Everything else (OAuth sk-ant-oat, setup-token format with #, etc.)
|
|
30
|
-
// goes into CLAUDE_CODE_OAUTH_TOKEN
|
|
31
|
-
// Mixing them causes "Invalid API key" errors
|
|
32
|
-
if (opts.token.startsWith("sk-ant-api")) {
|
|
33
|
-
env.ANTHROPIC_API_KEY = opts.token;
|
|
34
|
-
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
env.CLAUDE_CODE_OAUTH_TOKEN = opts.token;
|
|
38
|
-
delete env.ANTHROPIC_API_KEY;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// Resolve claude binary — check common install locations if not in PATH
|
|
42
|
-
const claudeBin = resolveClaude(env.PATH);
|
|
43
|
-
this.proc = spawn(claudeBin, args, {
|
|
44
|
-
cwd: opts.cwd ?? process.cwd(),
|
|
45
|
-
env,
|
|
46
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
47
|
-
});
|
|
48
|
-
this.proc.stdout.on("data", (chunk) => {
|
|
49
|
-
this.buffer += chunk.toString();
|
|
50
|
-
this.drainBuffer();
|
|
51
|
-
});
|
|
52
|
-
this.proc.stderr.on("data", (chunk) => {
|
|
53
|
-
this.emit("stderr", chunk.toString());
|
|
54
|
-
});
|
|
55
|
-
this.proc.on("exit", (code) => {
|
|
56
|
-
this._exited = true;
|
|
57
|
-
this.emit("exit", code);
|
|
58
|
-
});
|
|
59
|
-
this.proc.on("error", (err) => {
|
|
60
|
-
this.emit("error", err);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
sendPrompt(text) {
|
|
64
|
-
if (this._exited)
|
|
65
|
-
throw new Error("Claude process has exited");
|
|
66
|
-
const payload = JSON.stringify({
|
|
67
|
-
type: "user",
|
|
68
|
-
message: { role: "user", content: text },
|
|
69
|
-
});
|
|
70
|
-
this.proc.stdin.write(payload + "\n");
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Send an image (with optional text caption) to Claude via stream-json content blocks.
|
|
74
|
-
* mediaType: image/jpeg | image/png | image/gif | image/webp
|
|
75
|
-
*/
|
|
76
|
-
sendImage(base64Data, mediaType, caption) {
|
|
77
|
-
if (this._exited)
|
|
78
|
-
throw new Error("Claude process has exited");
|
|
79
|
-
const content = [];
|
|
80
|
-
if (caption) {
|
|
81
|
-
content.push({ type: "text", text: caption });
|
|
82
|
-
}
|
|
83
|
-
content.push({
|
|
84
|
-
type: "image",
|
|
85
|
-
source: {
|
|
86
|
-
type: "base64",
|
|
87
|
-
media_type: mediaType,
|
|
88
|
-
data: base64Data,
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
const payload = JSON.stringify({
|
|
92
|
-
type: "user",
|
|
93
|
-
message: { role: "user", content },
|
|
94
|
-
});
|
|
95
|
-
this.proc.stdin.write(payload + "\n");
|
|
96
|
-
}
|
|
97
|
-
kill() {
|
|
98
|
-
this.proc.kill();
|
|
99
|
-
}
|
|
100
|
-
get exited() {
|
|
101
|
-
return this._exited;
|
|
102
|
-
}
|
|
103
|
-
drainBuffer() {
|
|
104
|
-
const lines = this.buffer.split("\n");
|
|
105
|
-
// Last element may be incomplete — keep it
|
|
106
|
-
this.buffer = lines.pop() ?? "";
|
|
107
|
-
for (const line of lines) {
|
|
108
|
-
if (!line.trim())
|
|
109
|
-
continue;
|
|
110
|
-
let raw;
|
|
111
|
-
try {
|
|
112
|
-
raw = JSON.parse(line);
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// Non-JSON line (startup noise etc.) — ignore
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
// Emit usage events from Anthropic API stream events passed through by Claude CLI
|
|
119
|
-
if (raw.type === "message_start") {
|
|
120
|
-
const usage = (raw.message?.usage);
|
|
121
|
-
if (usage) {
|
|
122
|
-
this.emit("usage", {
|
|
123
|
-
inputTokens: usage.input_tokens ?? 0,
|
|
124
|
-
outputTokens: 0, // output_tokens at message_start is always 0
|
|
125
|
-
cacheReadTokens: usage.cache_read_input_tokens ?? 0,
|
|
126
|
-
cacheWriteTokens: usage.cache_creation_input_tokens ?? 0,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
else if (raw.type === "message_delta") {
|
|
131
|
-
const usage = raw.usage;
|
|
132
|
-
if (usage?.output_tokens) {
|
|
133
|
-
this.emit("usage", {
|
|
134
|
-
inputTokens: 0,
|
|
135
|
-
outputTokens: usage.output_tokens,
|
|
136
|
-
cacheReadTokens: 0,
|
|
137
|
-
cacheWriteTokens: 0,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
const msg = this.parseMessage(raw);
|
|
142
|
-
if (msg)
|
|
143
|
-
this.emit("message", msg);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
parseMessage(raw) {
|
|
147
|
-
const type = raw.type;
|
|
148
|
-
if (!type)
|
|
149
|
-
return null;
|
|
150
|
-
return {
|
|
151
|
-
type,
|
|
152
|
-
session_id: raw.session_id,
|
|
153
|
-
uuid: raw.uuid,
|
|
154
|
-
payload: raw,
|
|
155
|
-
raw,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Extract the text content from an assistant message payload.
|
|
161
|
-
* Handles both simple string content and content-block arrays.
|
|
162
|
-
*/
|
|
163
|
-
export function extractText(msg) {
|
|
164
|
-
const message = msg.payload.message;
|
|
165
|
-
if (!message) {
|
|
166
|
-
// result message type
|
|
167
|
-
if (msg.type === "result") {
|
|
168
|
-
return msg.payload.result ?? "";
|
|
169
|
-
}
|
|
170
|
-
return "";
|
|
171
|
-
}
|
|
172
|
-
const content = message.content;
|
|
173
|
-
if (typeof content === "string")
|
|
174
|
-
return content;
|
|
175
|
-
if (Array.isArray(content)) {
|
|
176
|
-
return content
|
|
177
|
-
.filter((b) => b.type === "text")
|
|
178
|
-
.map((b) => b.text)
|
|
179
|
-
.join("");
|
|
180
|
-
}
|
|
181
|
-
return "";
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Resolve the claude CLI binary path.
|
|
185
|
-
* Checks PATH entries + common npm global install locations.
|
|
186
|
-
*/
|
|
187
|
-
function resolveClaude(pathEnv) {
|
|
188
|
-
// Try PATH entries first
|
|
189
|
-
const dirs = (pathEnv ?? process.env.PATH ?? "").split(":");
|
|
190
|
-
for (const dir of dirs) {
|
|
191
|
-
const candidate = `${dir}/claude`;
|
|
192
|
-
if (existsSync(candidate))
|
|
193
|
-
return candidate;
|
|
194
|
-
}
|
|
195
|
-
// Common fallback locations
|
|
196
|
-
const fallbacks = [
|
|
197
|
-
`${process.env.HOME}/.npm-global/bin/claude`,
|
|
198
|
-
"/opt/homebrew/bin/claude",
|
|
199
|
-
"/usr/local/bin/claude",
|
|
200
|
-
"/usr/bin/claude",
|
|
201
|
-
];
|
|
202
|
-
for (const p of fallbacks) {
|
|
203
|
-
if (existsSync(p))
|
|
204
|
-
return p;
|
|
205
|
-
}
|
|
206
|
-
// Last resort — let the OS resolve it (will throw ENOENT if missing)
|
|
207
|
-
return "claude";
|
|
208
|
-
}
|
package/dist/cron.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cron job manager for cc-tg.
|
|
3
|
-
* Persists jobs to <cwd>/.cc-tg/crons.json.
|
|
4
|
-
* Fires prompts into Claude sessions on schedule.
|
|
5
|
-
*/
|
|
6
|
-
export interface CronJob {
|
|
7
|
-
id: string;
|
|
8
|
-
chatId: number;
|
|
9
|
-
intervalMs: number;
|
|
10
|
-
prompt: string;
|
|
11
|
-
createdAt: string;
|
|
12
|
-
schedule: string;
|
|
13
|
-
}
|
|
14
|
-
type FireCallback = (chatId: number, prompt: string) => void;
|
|
15
|
-
export declare class CronManager {
|
|
16
|
-
private jobs;
|
|
17
|
-
private storePath;
|
|
18
|
-
private fire;
|
|
19
|
-
constructor(cwd: string, fire: FireCallback);
|
|
20
|
-
/** Parse "every 30m", "every 2h", "every 1d" → ms */
|
|
21
|
-
static parseSchedule(schedule: string): number | null;
|
|
22
|
-
add(chatId: number, schedule: string, prompt: string): CronJob | null;
|
|
23
|
-
remove(chatId: number, id: string): boolean;
|
|
24
|
-
clearAll(chatId: number): number;
|
|
25
|
-
list(chatId: number): CronJob[];
|
|
26
|
-
update(chatId: number, id: string, updates: {
|
|
27
|
-
schedule?: string;
|
|
28
|
-
prompt?: string;
|
|
29
|
-
}): CronJob | null | false;
|
|
30
|
-
private persist;
|
|
31
|
-
private load;
|
|
32
|
-
}
|
|
33
|
-
export {};
|
package/dist/cron.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cron job manager for cc-tg.
|
|
3
|
-
* Persists jobs to <cwd>/.cc-tg/crons.json.
|
|
4
|
-
* Fires prompts into Claude sessions on schedule.
|
|
5
|
-
*/
|
|
6
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
7
|
-
import { join } from "path";
|
|
8
|
-
export class CronManager {
|
|
9
|
-
jobs = new Map();
|
|
10
|
-
storePath;
|
|
11
|
-
fire;
|
|
12
|
-
constructor(cwd, fire) {
|
|
13
|
-
this.storePath = join(cwd, ".cc-tg", "crons.json");
|
|
14
|
-
this.fire = fire;
|
|
15
|
-
this.load();
|
|
16
|
-
}
|
|
17
|
-
/** Parse "every 30m", "every 2h", "every 1d" → ms */
|
|
18
|
-
static parseSchedule(schedule) {
|
|
19
|
-
const m = schedule.trim().match(/^every\s+(\d+)(m|h|d)$/i);
|
|
20
|
-
if (!m)
|
|
21
|
-
return null;
|
|
22
|
-
const n = parseInt(m[1]);
|
|
23
|
-
const unit = m[2].toLowerCase();
|
|
24
|
-
if (unit === "m")
|
|
25
|
-
return n * 60_000;
|
|
26
|
-
if (unit === "h")
|
|
27
|
-
return n * 3_600_000;
|
|
28
|
-
if (unit === "d")
|
|
29
|
-
return n * 86_400_000;
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
add(chatId, schedule, prompt) {
|
|
33
|
-
const intervalMs = CronManager.parseSchedule(schedule);
|
|
34
|
-
if (!intervalMs)
|
|
35
|
-
return null;
|
|
36
|
-
const id = `${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
37
|
-
const job = { id, chatId, intervalMs, prompt, schedule, createdAt: new Date().toISOString() };
|
|
38
|
-
const timer = setInterval(() => {
|
|
39
|
-
console.log(`[cron:${id}] firing for chat=${chatId} prompt="${prompt}"`);
|
|
40
|
-
this.fire(chatId, prompt);
|
|
41
|
-
}, intervalMs);
|
|
42
|
-
this.jobs.set(id, { ...job, timer });
|
|
43
|
-
this.persist();
|
|
44
|
-
return job;
|
|
45
|
-
}
|
|
46
|
-
remove(chatId, id) {
|
|
47
|
-
const job = this.jobs.get(id);
|
|
48
|
-
if (!job || job.chatId !== chatId)
|
|
49
|
-
return false;
|
|
50
|
-
clearInterval(job.timer);
|
|
51
|
-
this.jobs.delete(id);
|
|
52
|
-
this.persist();
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
clearAll(chatId) {
|
|
56
|
-
let count = 0;
|
|
57
|
-
for (const [id, job] of this.jobs) {
|
|
58
|
-
if (job.chatId === chatId) {
|
|
59
|
-
clearInterval(job.timer);
|
|
60
|
-
this.jobs.delete(id);
|
|
61
|
-
count++;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (count)
|
|
65
|
-
this.persist();
|
|
66
|
-
return count;
|
|
67
|
-
}
|
|
68
|
-
list(chatId) {
|
|
69
|
-
return [...this.jobs.values()]
|
|
70
|
-
.filter((j) => j.chatId === chatId)
|
|
71
|
-
.map(({ timer: _t, ...j }) => j);
|
|
72
|
-
}
|
|
73
|
-
update(chatId, id, updates) {
|
|
74
|
-
const job = this.jobs.get(id);
|
|
75
|
-
if (!job || job.chatId !== chatId)
|
|
76
|
-
return false;
|
|
77
|
-
if (updates.schedule !== undefined) {
|
|
78
|
-
const intervalMs = CronManager.parseSchedule(updates.schedule);
|
|
79
|
-
if (!intervalMs)
|
|
80
|
-
return null;
|
|
81
|
-
job.intervalMs = intervalMs;
|
|
82
|
-
job.schedule = updates.schedule;
|
|
83
|
-
}
|
|
84
|
-
if (updates.prompt !== undefined) {
|
|
85
|
-
job.prompt = updates.prompt;
|
|
86
|
-
}
|
|
87
|
-
// Recreate timer so it uses updated intervalMs and always reads latest job.prompt
|
|
88
|
-
clearInterval(job.timer);
|
|
89
|
-
job.timer = setInterval(() => {
|
|
90
|
-
console.log(`[cron:${job.id}] firing for chat=${job.chatId} prompt="${job.prompt}"`);
|
|
91
|
-
this.fire(job.chatId, job.prompt);
|
|
92
|
-
}, job.intervalMs);
|
|
93
|
-
this.persist();
|
|
94
|
-
const { timer: _t, ...cronJob } = job;
|
|
95
|
-
return cronJob;
|
|
96
|
-
}
|
|
97
|
-
persist() {
|
|
98
|
-
try {
|
|
99
|
-
const dir = join(this.storePath, "..");
|
|
100
|
-
if (!existsSync(dir))
|
|
101
|
-
mkdirSync(dir, { recursive: true });
|
|
102
|
-
const data = [...this.jobs.values()].map(({ timer: _t, ...j }) => j);
|
|
103
|
-
writeFileSync(this.storePath, JSON.stringify(data, null, 2));
|
|
104
|
-
}
|
|
105
|
-
catch (err) {
|
|
106
|
-
console.error("[cron] persist error:", err.message);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
load() {
|
|
110
|
-
if (!existsSync(this.storePath))
|
|
111
|
-
return;
|
|
112
|
-
try {
|
|
113
|
-
const data = JSON.parse(readFileSync(this.storePath, "utf8"));
|
|
114
|
-
for (const job of data) {
|
|
115
|
-
const timer = setInterval(() => {
|
|
116
|
-
console.log(`[cron:${job.id}] firing for chat=${job.chatId} prompt="${job.prompt}"`);
|
|
117
|
-
this.fire(job.chatId, job.prompt);
|
|
118
|
-
}, job.intervalMs);
|
|
119
|
-
this.jobs.set(job.id, { ...job, timer });
|
|
120
|
-
}
|
|
121
|
-
console.log(`[cron] loaded ${data.length} jobs from disk`);
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
console.error("[cron] load error:", err.message);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
package/dist/index.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* cc-tg — Claude Code Telegram bot
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* npx @gonzih/cc-tg
|
|
7
|
-
*
|
|
8
|
-
* Required env:
|
|
9
|
-
* TELEGRAM_BOT_TOKEN — from @BotFather
|
|
10
|
-
* CLAUDE_CODE_TOKEN — your Claude Code OAuth token (or ANTHROPIC_API_KEY)
|
|
11
|
-
*
|
|
12
|
-
* Optional env:
|
|
13
|
-
* ALLOWED_USER_IDS — comma-separated Telegram user IDs (leave empty to allow all)
|
|
14
|
-
* CWD — working directory for Claude Code (default: process.cwd())
|
|
15
|
-
*/
|
|
16
|
-
export {};
|
package/dist/index.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* cc-tg — Claude Code Telegram bot
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* npx @gonzih/cc-tg
|
|
7
|
-
*
|
|
8
|
-
* Required env:
|
|
9
|
-
* TELEGRAM_BOT_TOKEN — from @BotFather
|
|
10
|
-
* CLAUDE_CODE_TOKEN — your Claude Code OAuth token (or ANTHROPIC_API_KEY)
|
|
11
|
-
*
|
|
12
|
-
* Optional env:
|
|
13
|
-
* ALLOWED_USER_IDS — comma-separated Telegram user IDs (leave empty to allow all)
|
|
14
|
-
* CWD — working directory for Claude Code (default: process.cwd())
|
|
15
|
-
*/
|
|
16
|
-
import { existsSync, writeFileSync, unlinkSync, readFileSync } from "fs";
|
|
17
|
-
import { tmpdir } from "os";
|
|
18
|
-
import { join } from "path";
|
|
19
|
-
import { CcTgBot } from "./bot.js";
|
|
20
|
-
const LOCK_FILE = join(tmpdir(), "cc-tg.lock");
|
|
21
|
-
function acquireLock() {
|
|
22
|
-
if (existsSync(LOCK_FILE)) {
|
|
23
|
-
try {
|
|
24
|
-
const pid = parseInt(readFileSync(LOCK_FILE, "utf8").trim());
|
|
25
|
-
process.kill(pid, 0);
|
|
26
|
-
console.error(`[cc-tg] Another instance is already running (PID ${pid}). Exiting.`);
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// PID is dead — stale lock, take over
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
writeFileSync(LOCK_FILE, String(process.pid));
|
|
34
|
-
process.on("exit", () => { try {
|
|
35
|
-
unlinkSync(LOCK_FILE);
|
|
36
|
-
}
|
|
37
|
-
catch { } });
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
if (!acquireLock()) {
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
function required(name) {
|
|
44
|
-
const val = process.env[name];
|
|
45
|
-
if (!val) {
|
|
46
|
-
console.error(`
|
|
47
|
-
ERROR: ${name} is not set.
|
|
48
|
-
|
|
49
|
-
cc-tg requires:
|
|
50
|
-
TELEGRAM_BOT_TOKEN — get one from @BotFather on Telegram
|
|
51
|
-
CLAUDE_CODE_TOKEN — your Claude Code OAuth token
|
|
52
|
-
|
|
53
|
-
Set them and run again:
|
|
54
|
-
TELEGRAM_BOT_TOKEN=xxx CLAUDE_CODE_TOKEN=yyy npx @gonzih/cc-tg
|
|
55
|
-
|
|
56
|
-
Or add to your shell profile / .env file.
|
|
57
|
-
`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
return val;
|
|
61
|
-
}
|
|
62
|
-
const telegramToken = required("TELEGRAM_BOT_TOKEN");
|
|
63
|
-
// Accept CLAUDE_CODE_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or ANTHROPIC_API_KEY
|
|
64
|
-
const claudeToken = process.env.CLAUDE_CODE_TOKEN ??
|
|
65
|
-
process.env.CLAUDE_CODE_OAUTH_TOKEN ??
|
|
66
|
-
process.env.ANTHROPIC_API_KEY;
|
|
67
|
-
if (!claudeToken) {
|
|
68
|
-
console.error(`
|
|
69
|
-
ERROR: No Claude token set. Set one of: CLAUDE_CODE_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, or ANTHROPIC_API_KEY.
|
|
70
|
-
|
|
71
|
-
Set one and run again:
|
|
72
|
-
TELEGRAM_BOT_TOKEN=xxx CLAUDE_CODE_TOKEN=yyy npx @gonzih/cc-tg
|
|
73
|
-
`);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
const allowedUserIds = process.env.ALLOWED_USER_IDS
|
|
77
|
-
? process.env.ALLOWED_USER_IDS.split(",").map((s) => parseInt(s.trim(), 10)).filter(Boolean)
|
|
78
|
-
: [];
|
|
79
|
-
const cwd = process.env.CWD ?? process.cwd();
|
|
80
|
-
const bot = new CcTgBot({
|
|
81
|
-
telegramToken,
|
|
82
|
-
claudeToken,
|
|
83
|
-
cwd,
|
|
84
|
-
allowedUserIds,
|
|
85
|
-
});
|
|
86
|
-
process.on("SIGINT", () => {
|
|
87
|
-
console.log("\nShutting down...");
|
|
88
|
-
bot.stop();
|
|
89
|
-
process.exit(0);
|
|
90
|
-
});
|
|
91
|
-
process.on("SIGTERM", () => {
|
|
92
|
-
bot.stop();
|
|
93
|
-
process.exit(0);
|
|
94
|
-
});
|
package/dist/voice.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Voice message transcription via whisper.cpp.
|
|
3
|
-
* Flow: Telegram OGG → ffmpeg convert to 16kHz WAV → whisper-cpp → text
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Transcribe a voice message from a Telegram file URL.
|
|
7
|
-
* Returns the transcribed text, or throws if whisper/ffmpeg not available.
|
|
8
|
-
*/
|
|
9
|
-
export declare function transcribeVoice(fileUrl: string): Promise<string>;
|
|
10
|
-
/**
|
|
11
|
-
* Check if voice transcription is available on this system.
|
|
12
|
-
*/
|
|
13
|
-
export declare function isVoiceAvailable(): boolean;
|
package/dist/voice.js
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Voice message transcription via whisper.cpp.
|
|
3
|
-
* Flow: Telegram OGG → ffmpeg convert to 16kHz WAV → whisper-cpp → text
|
|
4
|
-
*/
|
|
5
|
-
import { execFile } from "child_process";
|
|
6
|
-
import { promisify } from "util";
|
|
7
|
-
import { existsSync } from "fs";
|
|
8
|
-
import { unlink } from "fs/promises";
|
|
9
|
-
import { tmpdir } from "os";
|
|
10
|
-
import { join } from "path";
|
|
11
|
-
import https from "https";
|
|
12
|
-
import http from "http";
|
|
13
|
-
import { createWriteStream } from "fs";
|
|
14
|
-
const execFileAsync = promisify(execFile);
|
|
15
|
-
// Whisper model — small.en is fast and accurate enough for commands
|
|
16
|
-
// Falls back to base.en if small not found
|
|
17
|
-
const WHISPER_MODELS = [
|
|
18
|
-
"/opt/homebrew/share/whisper-cpp/ggml-small.en.bin",
|
|
19
|
-
"/opt/homebrew/share/whisper-cpp/ggml-small.bin",
|
|
20
|
-
"/opt/homebrew/share/whisper-cpp/ggml-base.en.bin",
|
|
21
|
-
"/opt/homebrew/share/whisper-cpp/ggml-base.bin",
|
|
22
|
-
// user-local
|
|
23
|
-
`${process.env.HOME}/.local/share/whisper-cpp/ggml-small.en.bin`,
|
|
24
|
-
`${process.env.HOME}/.local/share/whisper-cpp/ggml-base.en.bin`,
|
|
25
|
-
];
|
|
26
|
-
const WHISPER_BIN_CANDIDATES = [
|
|
27
|
-
"/opt/homebrew/bin/whisper-cli", // whisper-cpp brew formula installs as whisper-cli
|
|
28
|
-
"/opt/homebrew/bin/whisper-cpp",
|
|
29
|
-
"/usr/local/bin/whisper-cli",
|
|
30
|
-
"/usr/local/bin/whisper-cpp",
|
|
31
|
-
"/opt/homebrew/bin/whisper",
|
|
32
|
-
];
|
|
33
|
-
const FFMPEG_CANDIDATES = [
|
|
34
|
-
"/opt/homebrew/bin/ffmpeg",
|
|
35
|
-
"/usr/local/bin/ffmpeg",
|
|
36
|
-
"/usr/bin/ffmpeg",
|
|
37
|
-
];
|
|
38
|
-
function findBin(candidates) {
|
|
39
|
-
for (const p of candidates) {
|
|
40
|
-
if (existsSync(p))
|
|
41
|
-
return p;
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
function findModel() {
|
|
46
|
-
for (const p of WHISPER_MODELS) {
|
|
47
|
-
if (existsSync(p))
|
|
48
|
-
return p;
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
function downloadFile(url, dest) {
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const file = createWriteStream(dest);
|
|
55
|
-
const getter = url.startsWith("https") ? https : http;
|
|
56
|
-
getter.get(url, (res) => {
|
|
57
|
-
if (res.statusCode !== 200) {
|
|
58
|
-
reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
res.pipe(file);
|
|
62
|
-
file.on("finish", () => file.close(() => resolve()));
|
|
63
|
-
file.on("error", reject);
|
|
64
|
-
}).on("error", reject);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Transcribe a voice message from a Telegram file URL.
|
|
69
|
-
* Returns the transcribed text, or throws if whisper/ffmpeg not available.
|
|
70
|
-
*/
|
|
71
|
-
export async function transcribeVoice(fileUrl) {
|
|
72
|
-
const whisperBin = findBin(WHISPER_BIN_CANDIDATES);
|
|
73
|
-
if (!whisperBin)
|
|
74
|
-
throw new Error("whisper-cpp not found — install with: brew install whisper-cpp");
|
|
75
|
-
const ffmpegBin = findBin(FFMPEG_CANDIDATES);
|
|
76
|
-
if (!ffmpegBin)
|
|
77
|
-
throw new Error("ffmpeg not found — install with: brew install ffmpeg");
|
|
78
|
-
const model = findModel();
|
|
79
|
-
if (!model)
|
|
80
|
-
throw new Error("No whisper model found — run: whisper-cpp-download-ggml-model small.en");
|
|
81
|
-
const tmp = join(tmpdir(), `cc-tg-voice-${Date.now()}`);
|
|
82
|
-
const oggPath = `${tmp}.ogg`;
|
|
83
|
-
const wavPath = `${tmp}.wav`;
|
|
84
|
-
try {
|
|
85
|
-
// 1. Download OGG from Telegram
|
|
86
|
-
await downloadFile(fileUrl, oggPath);
|
|
87
|
-
// 2. Convert OGG → 16kHz mono WAV (whisper requirement)
|
|
88
|
-
await execFileAsync(ffmpegBin, [
|
|
89
|
-
"-y", "-i", oggPath,
|
|
90
|
-
"-ar", "16000",
|
|
91
|
-
"-ac", "1",
|
|
92
|
-
"-c:a", "pcm_s16le",
|
|
93
|
-
wavPath,
|
|
94
|
-
]);
|
|
95
|
-
// 3. Run whisper-cpp
|
|
96
|
-
const { stdout } = await execFileAsync(whisperBin, [
|
|
97
|
-
"-m", model,
|
|
98
|
-
"-f", wavPath,
|
|
99
|
-
"--no-timestamps",
|
|
100
|
-
"-l", "auto",
|
|
101
|
-
"--output-txt",
|
|
102
|
-
]);
|
|
103
|
-
// whisper outputs to stdout — strip leading/trailing whitespace and [BLANK_AUDIO] artifacts
|
|
104
|
-
const text = stdout
|
|
105
|
-
.replace(/\[BLANK_AUDIO\]/gi, "")
|
|
106
|
-
.replace(/\[.*?\]/g, "") // remove timestamp artifacts
|
|
107
|
-
.trim();
|
|
108
|
-
return text || "[empty transcription]";
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
// Cleanup temp files
|
|
112
|
-
await unlink(oggPath).catch(() => { });
|
|
113
|
-
await unlink(wavPath).catch(() => { });
|
|
114
|
-
await unlink(`${wavPath}.txt`).catch(() => { });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Check if voice transcription is available on this system.
|
|
119
|
-
*/
|
|
120
|
-
export function isVoiceAvailable() {
|
|
121
|
-
return (findBin(WHISPER_BIN_CANDIDATES) !== null &&
|
|
122
|
-
findBin(FFMPEG_CANDIDATES) !== null &&
|
|
123
|
-
findModel() !== null);
|
|
124
|
-
}
|