claude-b 0.3.2
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 +229 -0
- package/README.md +877 -0
- package/assets/Claude-B.png +0 -0
- package/bin/cb +3 -0
- package/bin/cb-notify.sh +156 -0
- package/dist/chunk-6D3ICY25.js +303 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1643 -0
- package/dist/daemon/index.d.ts +2 -0
- package/dist/daemon/index.js +5730 -0
- package/eslint.config.js +44 -0
- package/package.json +68 -0
- package/scripts/install.sh +124 -0
- package/start-daemon.sh +15 -0
- package/website/deploy.sh +112 -0
- package/website/index.html +73 -0
- package/website/install.sh +124 -0
- package/website/nginx.conf +30 -0
|
Binary file
|
package/bin/cb
ADDED
package/bin/cb-notify.sh
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Claude Code Stop hook → Claude-B /api/notify → Telegram
|
|
3
|
+
#
|
|
4
|
+
# Fires every time a top-level Claude Code response finishes in any tmux pane.
|
|
5
|
+
# Extracts the last assistant text from the session transcript, tags it with
|
|
6
|
+
# the tmux target (session:window.pane) + pane title (Claude's slug), and
|
|
7
|
+
# POSTs the payload to Claude-B's REST API. Claude-B forwards it to Telegram
|
|
8
|
+
# via the existing bot.broadcastNotification path.
|
|
9
|
+
#
|
|
10
|
+
# Design rules:
|
|
11
|
+
# - NEVER fail the host Claude session. Always exit 0 on any error.
|
|
12
|
+
# - Skip silently if not running inside tmux.
|
|
13
|
+
# - Skip silently if Claude-B daemon / REST / API key is unavailable.
|
|
14
|
+
#
|
|
15
|
+
# Installed as: ~/.claude/settings.json hooks.Stop → this script
|
|
16
|
+
# "hooks": { "Stop": [{ "hooks": [{ "type": "command",
|
|
17
|
+
# "command": "$HOME/Claude-B/bin/cb-notify.sh" }] }] }
|
|
18
|
+
|
|
19
|
+
set +e # tolerate errors — we never want to break the host session
|
|
20
|
+
|
|
21
|
+
CB_URL="${CB_NOTIFY_URL:-http://127.0.0.1:3847/api/notify}"
|
|
22
|
+
CB_KEY_FILE="${CB_API_KEY_FILE:-$HOME/.claude-b/api.key}"
|
|
23
|
+
MAX_RESULT_CHARS=3000
|
|
24
|
+
LOG_FILE="${CB_NOTIFY_LOG:-$HOME/.claude-b/cb-notify.log}"
|
|
25
|
+
|
|
26
|
+
# ─── Read Claude Code hook payload from stdin ───────────────────────────────
|
|
27
|
+
payload=$(cat)
|
|
28
|
+
[[ -z "$payload" ]] && exit 0
|
|
29
|
+
|
|
30
|
+
transcript_path=$(jq -r '.transcript_path // empty' <<<"$payload" 2>/dev/null)
|
|
31
|
+
claude_session_id=$(jq -r '.session_id // empty' <<<"$payload" 2>/dev/null)
|
|
32
|
+
hook_cwd=$(jq -r '.cwd // empty' <<<"$payload" 2>/dev/null)
|
|
33
|
+
|
|
34
|
+
# ─── We only notify for tmux-hosted sessions ────────────────────────────────
|
|
35
|
+
# $TMUX is set by tmux itself for any process running inside a pane.
|
|
36
|
+
if [[ -z "${TMUX:-}" ]]; then
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# ─── Derive the tmux target + human label ───────────────────────────────────
|
|
41
|
+
# Prefer $TMUX_PANE (the unique pane id like %42) for lookup, then ask tmux
|
|
42
|
+
# for the stable session:window.pane target and the pane title.
|
|
43
|
+
pane_ref="${TMUX_PANE:-}"
|
|
44
|
+
if [[ -n "$pane_ref" ]]; then
|
|
45
|
+
tmux_target=$(tmux display-message -p -t "$pane_ref" '#S:#I.#P' 2>/dev/null)
|
|
46
|
+
pane_title=$(tmux display-message -p -t "$pane_ref" '#T' 2>/dev/null)
|
|
47
|
+
else
|
|
48
|
+
tmux_target=$(tmux display-message -p '#S:#I.#P' 2>/dev/null)
|
|
49
|
+
pane_title=$(tmux display-message -p '#T' 2>/dev/null)
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
if [[ -z "$tmux_target" ]]; then
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
session_label="$tmux_target"
|
|
57
|
+
if [[ -n "$pane_title" ]]; then
|
|
58
|
+
session_label="${tmux_target} ${pane_title}"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# ─── Extract last assistant text from the transcript ────────────────────────
|
|
62
|
+
# Transcript is JSONL where each line is a typed record. We want the most
|
|
63
|
+
# recent assistant message that has at least one text content block. Some
|
|
64
|
+
# assistant turns end with tool_use blocks only (e.g. a final Edit/Bash
|
|
65
|
+
# without commentary); those are skipped backwards until a text-bearing
|
|
66
|
+
# message is found.
|
|
67
|
+
#
|
|
68
|
+
# Small delay: the Stop hook fires right after the response completes but
|
|
69
|
+
# the transcript file may still have unflushed data in kernel/process
|
|
70
|
+
# buffers. Without this, grep can miss the final lines and return an
|
|
71
|
+
# intermediate assistant message instead of the actual summary.
|
|
72
|
+
sleep 0.5
|
|
73
|
+
|
|
74
|
+
last_assistant=""
|
|
75
|
+
if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
76
|
+
last_assistant=$(
|
|
77
|
+
grep '"type":"assistant"' "$transcript_path" 2>/dev/null \
|
|
78
|
+
| tac \
|
|
79
|
+
| while IFS= read -r line; do
|
|
80
|
+
text=$(jq -r '
|
|
81
|
+
.message.content // []
|
|
82
|
+
| map(select(.type=="text"))
|
|
83
|
+
| map(.text)
|
|
84
|
+
| join("\n")
|
|
85
|
+
' <<<"$line" 2>/dev/null)
|
|
86
|
+
if [[ -n "$text" ]]; then
|
|
87
|
+
echo "$text"
|
|
88
|
+
break
|
|
89
|
+
fi
|
|
90
|
+
done
|
|
91
|
+
)
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# Grab the most recent turn_duration record if present (Claude writes one
|
|
95
|
+
# system:turn_duration line after each completed assistant turn).
|
|
96
|
+
duration_ms=""
|
|
97
|
+
if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
98
|
+
duration_ms=$(
|
|
99
|
+
grep '"subtype":"turn_duration"' "$transcript_path" 2>/dev/null \
|
|
100
|
+
| tail -1 \
|
|
101
|
+
| jq -r '.durationMs // empty' 2>/dev/null
|
|
102
|
+
)
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Fallback body if no assistant message had text at all
|
|
106
|
+
if [[ -z "$last_assistant" ]]; then
|
|
107
|
+
last_assistant="(session completed — no assistant text in transcript)"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# Truncate for the mobile display
|
|
111
|
+
if [[ ${#last_assistant} -gt $MAX_RESULT_CHARS ]]; then
|
|
112
|
+
last_assistant="${last_assistant:0:$MAX_RESULT_CHARS}…"
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# ─── Look up API key and POST to /api/notify ────────────────────────────────
|
|
116
|
+
if [[ ! -r "$CB_KEY_FILE" ]]; then
|
|
117
|
+
exit 0
|
|
118
|
+
fi
|
|
119
|
+
api_key=$(<"$CB_KEY_FILE")
|
|
120
|
+
[[ -z "$api_key" ]] && exit 0
|
|
121
|
+
|
|
122
|
+
# Build JSON body safely via jq (handles quoting + newlines).
|
|
123
|
+
# transcriptPath is cached by the daemon so the Telegram voice pipeline can
|
|
124
|
+
# later ground optimizePrompt in real session history.
|
|
125
|
+
body=$(jq -n \
|
|
126
|
+
--arg sessionId "tmux:${tmux_target}" \
|
|
127
|
+
--arg sessionName "$session_label" \
|
|
128
|
+
--arg goal "$hook_cwd" \
|
|
129
|
+
--arg result "$last_assistant" \
|
|
130
|
+
--arg duration "$duration_ms" \
|
|
131
|
+
--arg transcriptPath "$transcript_path" \
|
|
132
|
+
'{
|
|
133
|
+
sessionId: $sessionId,
|
|
134
|
+
sessionName: $sessionName,
|
|
135
|
+
type: "prompt.completed",
|
|
136
|
+
goal: $goal,
|
|
137
|
+
exitCode: 0,
|
|
138
|
+
resultPreview: $result
|
|
139
|
+
}
|
|
140
|
+
+ (if $duration != "" then { durationMs: ($duration | tonumber) } else {} end)
|
|
141
|
+
+ (if $transcriptPath != "" then { transcriptPath: $transcriptPath } else {} end)')
|
|
142
|
+
|
|
143
|
+
# Fire and forget — short timeout, discard output. Log to a rolling file so
|
|
144
|
+
# the user can debug without cluttering their terminal.
|
|
145
|
+
{
|
|
146
|
+
echo "[$(date -Iseconds)] → $tmux_target (${#last_assistant} chars)"
|
|
147
|
+
curl -sS -m 5 -X POST "$CB_URL" \
|
|
148
|
+
-H 'Content-Type: application/json' \
|
|
149
|
+
-H "X-Claude-B-Key: ${api_key}" \
|
|
150
|
+
--data-binary "$body" 2>&1
|
|
151
|
+
echo
|
|
152
|
+
} >>"$LOG_FILE" 2>&1 &
|
|
153
|
+
|
|
154
|
+
# Detach background curl so we exit immediately — never block host session.
|
|
155
|
+
disown 2>/dev/null || true
|
|
156
|
+
exit 0
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
// src/config/env.ts
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
function parseEnvFile(path) {
|
|
13
|
+
const out = {};
|
|
14
|
+
const raw = readFileSync(path, "utf-8");
|
|
15
|
+
for (const rawLine of raw.split(/\r?\n/)) {
|
|
16
|
+
const line = rawLine.trim();
|
|
17
|
+
if (!line || line.startsWith("#")) continue;
|
|
18
|
+
const eq = line.indexOf("=");
|
|
19
|
+
if (eq === -1) continue;
|
|
20
|
+
const key = line.slice(0, eq).trim();
|
|
21
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) continue;
|
|
22
|
+
let value = line.slice(eq + 1).trim();
|
|
23
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
24
|
+
value = value.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
out[key] = value;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function loadEnv() {
|
|
31
|
+
const dataDir = process.env.CB_DATA_DIR || join(homedir(), ".claude-b");
|
|
32
|
+
const sources = [
|
|
33
|
+
join(dataDir, ".env"),
|
|
34
|
+
join(process.cwd(), ".env")
|
|
35
|
+
];
|
|
36
|
+
for (const path of sources) {
|
|
37
|
+
if (!existsSync(path)) continue;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = parseEnvFile(path);
|
|
40
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
41
|
+
if (process.env[key] === void 0) {
|
|
42
|
+
process.env[key] = value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (process.env.CB_REST_HOST === void 0 && process.env.REST_HOST) {
|
|
49
|
+
process.env.CB_REST_HOST = process.env.REST_HOST;
|
|
50
|
+
}
|
|
51
|
+
if (process.env.CB_REST_PORT === void 0 && process.env.REST_PORT) {
|
|
52
|
+
process.env.CB_REST_PORT = process.env.REST_PORT;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getDataDir() {
|
|
56
|
+
return process.env.CB_DATA_DIR || join(homedir(), ".claude-b");
|
|
57
|
+
}
|
|
58
|
+
function envTemplate() {
|
|
59
|
+
return `# Claude-B configuration
|
|
60
|
+
# Precedence: process env > ~/.claude-b/.env > ./.env
|
|
61
|
+
# Uncomment and fill the vars you need.
|
|
62
|
+
|
|
63
|
+
# --- Required for Claude Code ---
|
|
64
|
+
# ANTHROPIC_API_KEY=sk-ant-...
|
|
65
|
+
|
|
66
|
+
# --- Optional: voice pipeline (Telegram voice notes) ---
|
|
67
|
+
# OPENAI_API_KEY=sk-...
|
|
68
|
+
# SPEECHMATICS_API_KEY=
|
|
69
|
+
# DEEPGRAM_API_KEY=
|
|
70
|
+
|
|
71
|
+
# --- Optional: Telegram integration ---
|
|
72
|
+
# TELEGRAM_BOT_TOKEN=123456:ABC...
|
|
73
|
+
# TELEGRAM_ALLOWED_CHAT_IDS=123456789,987654321
|
|
74
|
+
|
|
75
|
+
# --- Optional: Claude-B infrastructure ---
|
|
76
|
+
# CB_DATA_DIR=~/.claude-b
|
|
77
|
+
# CB_REST_HOST=127.0.0.1
|
|
78
|
+
# CB_REST_PORT=3847
|
|
79
|
+
# CB_REST_API_KEY=
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/utils/claude-detector.ts
|
|
84
|
+
import { existsSync as existsSync2 } from "fs";
|
|
85
|
+
import { execSync } from "child_process";
|
|
86
|
+
import { homedir as homedir2 } from "os";
|
|
87
|
+
import { join as join2 } from "path";
|
|
88
|
+
var cachedInstallation = null;
|
|
89
|
+
var CLAUDE_LOCATIONS = [
|
|
90
|
+
// Native installer (new)
|
|
91
|
+
{ path: join2(homedir2(), ".claude", "bin", "claude"), type: "native" },
|
|
92
|
+
// npm global (common)
|
|
93
|
+
{ path: join2(homedir2(), ".npm-global", "bin", "claude"), type: "npm-global" },
|
|
94
|
+
// Local bin
|
|
95
|
+
{ path: join2(homedir2(), ".local", "bin", "claude"), type: "local" },
|
|
96
|
+
// Homebrew (macOS)
|
|
97
|
+
{ path: "/opt/homebrew/bin/claude", type: "homebrew" },
|
|
98
|
+
{ path: "/usr/local/bin/claude", type: "homebrew" },
|
|
99
|
+
// Linux system
|
|
100
|
+
{ path: "/usr/bin/claude", type: "path" }
|
|
101
|
+
];
|
|
102
|
+
function detectClaude() {
|
|
103
|
+
if (cachedInstallation) {
|
|
104
|
+
return cachedInstallation;
|
|
105
|
+
}
|
|
106
|
+
const envPath = process.env.CLAUDE_PATH;
|
|
107
|
+
if (envPath && existsSync2(envPath)) {
|
|
108
|
+
cachedInstallation = { path: envPath, type: "env" };
|
|
109
|
+
cachedInstallation.version = getClaudeVersion(envPath);
|
|
110
|
+
return cachedInstallation;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const whichResult = execSync("which claude 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
114
|
+
if (whichResult && existsSync2(whichResult)) {
|
|
115
|
+
cachedInstallation = { path: whichResult, type: "path" };
|
|
116
|
+
cachedInstallation.version = getClaudeVersion(whichResult);
|
|
117
|
+
return cachedInstallation;
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
for (const location of CLAUDE_LOCATIONS) {
|
|
122
|
+
if (existsSync2(location.path)) {
|
|
123
|
+
cachedInstallation = { path: location.path, type: location.type };
|
|
124
|
+
cachedInstallation.version = getClaudeVersion(location.path);
|
|
125
|
+
return cachedInstallation;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function getClaudeVersion(claudePath) {
|
|
131
|
+
try {
|
|
132
|
+
const result = execSync(`"${claudePath}" --version 2>/dev/null`, { encoding: "utf-8" });
|
|
133
|
+
const match = result.match(/(\d+\.\d+\.\d+)/);
|
|
134
|
+
return match ? match[1] : void 0;
|
|
135
|
+
} catch {
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function getClaudePath() {
|
|
140
|
+
const installation = detectClaude();
|
|
141
|
+
if (!installation) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"Claude Code not found. Please install it:\n npm install -g @anthropic-ai/claude-code\nOr set CLAUDE_PATH environment variable to the claude executable path."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return installation.path;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/telegram/config.ts
|
|
150
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
151
|
+
import { existsSync as existsSync3 } from "fs";
|
|
152
|
+
var DEFAULT_CONFIG = {
|
|
153
|
+
token: "",
|
|
154
|
+
enabled: false,
|
|
155
|
+
chatIds: [],
|
|
156
|
+
sessionMap: {},
|
|
157
|
+
pendingPrompts: {},
|
|
158
|
+
resultMap: {}
|
|
159
|
+
};
|
|
160
|
+
var TelegramConfigManager = class {
|
|
161
|
+
configPath;
|
|
162
|
+
configDir;
|
|
163
|
+
config = { ...DEFAULT_CONFIG };
|
|
164
|
+
constructor(configDir) {
|
|
165
|
+
this.configDir = configDir;
|
|
166
|
+
this.configPath = `${configDir}/telegram.json`;
|
|
167
|
+
}
|
|
168
|
+
async load() {
|
|
169
|
+
if (!existsSync3(this.configPath)) {
|
|
170
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
171
|
+
return this.config;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const content = await readFile(this.configPath, "utf-8");
|
|
175
|
+
this.config = { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
176
|
+
if (this.config.speechmaticsApiKey && !this.config.sttProvider) {
|
|
177
|
+
this.config.sttProvider = { provider: "speechmatics", apiKey: this.config.speechmaticsApiKey };
|
|
178
|
+
await this.save();
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
182
|
+
}
|
|
183
|
+
return this.config;
|
|
184
|
+
}
|
|
185
|
+
async save() {
|
|
186
|
+
await mkdir(this.configDir, { recursive: true });
|
|
187
|
+
await writeFile(this.configPath, JSON.stringify(this.config, null, 2) + "\n");
|
|
188
|
+
}
|
|
189
|
+
get() {
|
|
190
|
+
return this.config;
|
|
191
|
+
}
|
|
192
|
+
shouldForwardSession() {
|
|
193
|
+
return this.config.forwardAllSessions ?? true;
|
|
194
|
+
}
|
|
195
|
+
async setForwardAllSessions(enabled) {
|
|
196
|
+
this.config.forwardAllSessions = enabled;
|
|
197
|
+
await this.save();
|
|
198
|
+
}
|
|
199
|
+
async setToken(token) {
|
|
200
|
+
this.config.token = token;
|
|
201
|
+
this.config.enabled = true;
|
|
202
|
+
await this.save();
|
|
203
|
+
}
|
|
204
|
+
async disable() {
|
|
205
|
+
this.config.enabled = false;
|
|
206
|
+
this.config.token = "";
|
|
207
|
+
this.config.chatIds = [];
|
|
208
|
+
this.config.sessionMap = {};
|
|
209
|
+
await this.save();
|
|
210
|
+
}
|
|
211
|
+
async addChatId(chatId) {
|
|
212
|
+
if (!this.config.chatIds.includes(chatId)) {
|
|
213
|
+
this.config.chatIds.push(chatId);
|
|
214
|
+
await this.save();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async mapMessage(telegramMessageId, claudeSessionId) {
|
|
218
|
+
this.config.sessionMap[telegramMessageId] = claudeSessionId;
|
|
219
|
+
const keys = Object.keys(this.config.sessionMap);
|
|
220
|
+
if (keys.length > 500) {
|
|
221
|
+
for (const key of keys.slice(0, keys.length - 500)) {
|
|
222
|
+
delete this.config.sessionMap[key];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
await this.save();
|
|
226
|
+
}
|
|
227
|
+
getSessionForMessage(telegramMessageId) {
|
|
228
|
+
return this.config.sessionMap[telegramMessageId];
|
|
229
|
+
}
|
|
230
|
+
// Voice config
|
|
231
|
+
async setSTTProvider(config) {
|
|
232
|
+
this.config.sttProvider = config;
|
|
233
|
+
if (config.provider === "speechmatics") {
|
|
234
|
+
this.config.speechmaticsApiKey = config.apiKey;
|
|
235
|
+
}
|
|
236
|
+
await this.save();
|
|
237
|
+
}
|
|
238
|
+
/** @deprecated Use setSTTProvider instead */
|
|
239
|
+
async setSpeechmaticsKey(key) {
|
|
240
|
+
await this.setSTTProvider({ provider: "speechmatics", apiKey: key });
|
|
241
|
+
}
|
|
242
|
+
async setAIProvider(config) {
|
|
243
|
+
this.config.aiProvider = config;
|
|
244
|
+
await this.save();
|
|
245
|
+
}
|
|
246
|
+
// Pending prompts (awaiting user confirmation)
|
|
247
|
+
addPendingPrompt(messageId, pending) {
|
|
248
|
+
this.config.pendingPrompts[messageId] = pending;
|
|
249
|
+
const now = Date.now();
|
|
250
|
+
const entries = Object.entries(this.config.pendingPrompts);
|
|
251
|
+
for (const [key, val] of entries) {
|
|
252
|
+
if (now - val.timestamp > 10 * 60 * 1e3) {
|
|
253
|
+
delete this.config.pendingPrompts[key];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const keys = Object.keys(this.config.pendingPrompts);
|
|
257
|
+
if (keys.length > 50) {
|
|
258
|
+
for (const key of keys.slice(0, keys.length - 50)) {
|
|
259
|
+
delete this.config.pendingPrompts[key];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
getPendingPrompt(messageId) {
|
|
264
|
+
const pending = this.config.pendingPrompts[messageId];
|
|
265
|
+
if (pending && Date.now() - pending.timestamp > 10 * 60 * 1e3) {
|
|
266
|
+
delete this.config.pendingPrompts[messageId];
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
return pending;
|
|
270
|
+
}
|
|
271
|
+
removePendingPrompt(messageId) {
|
|
272
|
+
delete this.config.pendingPrompts[messageId];
|
|
273
|
+
}
|
|
274
|
+
updatePendingPrompt(messageId, optimizedPrompt) {
|
|
275
|
+
const pending = this.config.pendingPrompts[messageId];
|
|
276
|
+
if (pending) {
|
|
277
|
+
pending.optimizedPrompt = optimizedPrompt;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// Result map (for TTS playback of notification results)
|
|
281
|
+
storeResult(messageId, resultText) {
|
|
282
|
+
this.config.resultMap[messageId] = resultText.slice(0, 5e3);
|
|
283
|
+
const keys = Object.keys(this.config.resultMap);
|
|
284
|
+
if (keys.length > 200) {
|
|
285
|
+
for (const key of keys.slice(0, keys.length - 200)) {
|
|
286
|
+
delete this.config.resultMap[key];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
getResult(messageId) {
|
|
291
|
+
return this.config.resultMap[messageId];
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
export {
|
|
296
|
+
__require,
|
|
297
|
+
loadEnv,
|
|
298
|
+
getDataDir,
|
|
299
|
+
envTemplate,
|
|
300
|
+
detectClaude,
|
|
301
|
+
getClaudePath,
|
|
302
|
+
TelegramConfigManager
|
|
303
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|