cc-claw 0.4.4 → 0.4.5
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 +6 -3
- package/dist/cli.js +436 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Send text, voice, photos, documents, or videos. Switch backends with `/claude`,
|
|
|
15
15
|
- **Agent templates** — Define reusable agent personas as markdown files (`~/.cc-claw/agents/*.md`). Security reviewers, content writers, researchers — spawn them by name.
|
|
16
16
|
- **Scheduling** — Cron jobs, one-shot tasks, and intervals. Per-job backend, model, and delivery (Telegram, webhook, or silent).
|
|
17
17
|
- **Memory** — Hybrid vector + keyword search. The agent remembers what you tell it across sessions, with salience decay.
|
|
18
|
-
- **Voice** — Send voice messages, get voice replies. Groq Whisper for transcription
|
|
18
|
+
- **Voice** — Send voice messages, get voice replies. Groq Whisper for transcription. Choose from ElevenLabs, Grok (xAI), or macOS for synthesis.
|
|
19
19
|
- **50+ CLI commands** — Full system management from terminal. Every command supports `--json` for scripting.
|
|
20
20
|
- **MCP support** — Model Context Protocol servers extend the agent with external tools and data.
|
|
21
21
|
- **Cross-channel awareness** — Actions from CLI are visible to Telegram and vice versa via the activity log.
|
|
@@ -166,6 +166,8 @@ Read commands work offline (direct DB access). Write commands require the daemon
|
|
|
166
166
|
| `/permissions` | Permission mode (yolo/safe/readonly/plan) |
|
|
167
167
|
| `/tools` | Configure allowed tools |
|
|
168
168
|
| `/voice` | Toggle voice replies |
|
|
169
|
+
| `/voice_config` | Configure voice provider and voice |
|
|
170
|
+
| `/response_style` | Set the AI response style (concise/normal/detailed) |
|
|
169
171
|
| `/verbose` | Tool visibility level |
|
|
170
172
|
| `/limits` | Usage limits per backend |
|
|
171
173
|
|
|
@@ -186,7 +188,7 @@ Read commands work offline (direct DB access). Write commands require the daemon
|
|
|
186
188
|
```
|
|
187
189
|
Telegram → TelegramChannel → router (handleMessage)
|
|
188
190
|
├── command → handleCommand
|
|
189
|
-
├── voice → Groq Whisper STT → askAgent
|
|
191
|
+
├── voice → Groq Whisper STT → askAgent → TTS (ElevenLabs/Grok/macOS)
|
|
190
192
|
├── photo/document/video → handleMedia → askAgent
|
|
191
193
|
└── text → askAgent → sendResponse
|
|
192
194
|
└── [SEND_FILE:/path] → channel.sendFile
|
|
@@ -251,7 +253,8 @@ Set in `~/.cc-claw/.env` (created by `cc-claw setup`):
|
|
|
251
253
|
| `CLOUD_ML_REGION` | For Claude | e.g., `us-east5` |
|
|
252
254
|
| `ANTHROPIC_VERTEX_PROJECT_ID` | For Claude | GCP project ID |
|
|
253
255
|
| `GROQ_API_KEY` | No | Voice transcription |
|
|
254
|
-
| `ELEVENLABS_API_KEY` | No | Voice replies |
|
|
256
|
+
| `ELEVENLABS_API_KEY` | No | Voice replies (ElevenLabs) |
|
|
257
|
+
| `XAI_API_KEY` | No | Voice replies (Grok/xAI) |
|
|
255
258
|
| `GEMINI_API_KEY` | No | Video analysis |
|
|
256
259
|
| `DASHBOARD_ENABLED` | No | `1` for web dashboard on port 3141 |
|
|
257
260
|
| `EMBEDDING_PROVIDER` | No | `ollama` (default), `gemini`, `openai`, `off` |
|
package/dist/cli.js
CHANGED
|
@@ -48,7 +48,7 @@ var VERSION;
|
|
|
48
48
|
var init_version = __esm({
|
|
49
49
|
"src/version.ts"() {
|
|
50
50
|
"use strict";
|
|
51
|
-
VERSION = true ? "0.4.
|
|
51
|
+
VERSION = true ? "0.4.5" : (() => {
|
|
52
52
|
try {
|
|
53
53
|
return JSON.parse(readFileSync(join2(process.cwd(), "package.json"), "utf-8")).version ?? "unknown";
|
|
54
54
|
} catch {
|
|
@@ -968,6 +968,7 @@ __export(store_exports3, {
|
|
|
968
968
|
getRecentBookmarks: () => getRecentBookmarks,
|
|
969
969
|
getRecentMemories: () => getRecentMemories,
|
|
970
970
|
getRecentMessageLog: () => getRecentMessageLog,
|
|
971
|
+
getResponseStyle: () => getResponseStyle,
|
|
971
972
|
getSessionId: () => getSessionId,
|
|
972
973
|
getSessionStartedAt: () => getSessionStartedAt,
|
|
973
974
|
getSessionSummariesWithoutEmbeddings: () => getSessionSummariesWithoutEmbeddings,
|
|
@@ -1009,6 +1010,7 @@ __export(store_exports3, {
|
|
|
1009
1010
|
setHeartbeatConfig: () => setHeartbeatConfig,
|
|
1010
1011
|
setMode: () => setMode,
|
|
1011
1012
|
setModel: () => setModel,
|
|
1013
|
+
setResponseStyle: () => setResponseStyle,
|
|
1012
1014
|
setSessionId: () => setSessionId,
|
|
1013
1015
|
setSessionStartedAt: () => setSessionStartedAt,
|
|
1014
1016
|
setSummarizer: () => setSummarizer,
|
|
@@ -1358,9 +1360,19 @@ function initDatabase() {
|
|
|
1358
1360
|
db.exec(`
|
|
1359
1361
|
CREATE TABLE IF NOT EXISTS chat_voice (
|
|
1360
1362
|
chat_id TEXT PRIMARY KEY,
|
|
1361
|
-
enabled INTEGER NOT NULL DEFAULT 0
|
|
1363
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
1364
|
+
provider TEXT DEFAULT 'elevenlabs',
|
|
1365
|
+
voice_id TEXT
|
|
1362
1366
|
);
|
|
1363
1367
|
`);
|
|
1368
|
+
try {
|
|
1369
|
+
db.exec(`ALTER TABLE chat_voice ADD COLUMN provider TEXT DEFAULT 'elevenlabs'`);
|
|
1370
|
+
} catch {
|
|
1371
|
+
}
|
|
1372
|
+
try {
|
|
1373
|
+
db.exec(`ALTER TABLE chat_voice ADD COLUMN voice_id TEXT`);
|
|
1374
|
+
} catch {
|
|
1375
|
+
}
|
|
1364
1376
|
db.exec(`
|
|
1365
1377
|
CREATE TABLE IF NOT EXISTS backend_limits (
|
|
1366
1378
|
backend TEXT NOT NULL,
|
|
@@ -1468,6 +1480,12 @@ function initDatabase() {
|
|
|
1468
1480
|
initMcpTables(db);
|
|
1469
1481
|
initActivityTable(db);
|
|
1470
1482
|
initIdentityTables(db);
|
|
1483
|
+
db.exec(`
|
|
1484
|
+
CREATE TABLE IF NOT EXISTS chat_response_style (
|
|
1485
|
+
chat_id TEXT PRIMARY KEY,
|
|
1486
|
+
style TEXT NOT NULL DEFAULT 'normal'
|
|
1487
|
+
);
|
|
1488
|
+
`);
|
|
1471
1489
|
}
|
|
1472
1490
|
function getDb() {
|
|
1473
1491
|
return db;
|
|
@@ -1600,6 +1618,17 @@ function applySalienceDecay() {
|
|
|
1600
1618
|
db.prepare("DELETE FROM session_summaries WHERE salience < 0.1").run();
|
|
1601
1619
|
db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('last_salience_decay', ?)").run(now);
|
|
1602
1620
|
}
|
|
1621
|
+
function getResponseStyle(chatId) {
|
|
1622
|
+
const row = db.prepare("SELECT style FROM chat_response_style WHERE chat_id = ?").get(chatId);
|
|
1623
|
+
return row?.style ?? "normal";
|
|
1624
|
+
}
|
|
1625
|
+
function setResponseStyle(chatId, style) {
|
|
1626
|
+
db.prepare(`
|
|
1627
|
+
INSERT INTO chat_response_style (chat_id, style)
|
|
1628
|
+
VALUES (?, ?)
|
|
1629
|
+
ON CONFLICT(chat_id) DO UPDATE SET style = excluded.style
|
|
1630
|
+
`).run(chatId, style);
|
|
1631
|
+
}
|
|
1603
1632
|
function getSessionId(chatId) {
|
|
1604
1633
|
const row = db.prepare(
|
|
1605
1634
|
"SELECT session_id FROM sessions WHERE chat_id = ?"
|
|
@@ -3219,7 +3248,7 @@ var init_inject = __esm({
|
|
|
3219
3248
|
});
|
|
3220
3249
|
|
|
3221
3250
|
// src/bootstrap/defaults.ts
|
|
3222
|
-
var DEFAULT_SOUL, DEFAULT_USER;
|
|
3251
|
+
var DEFAULT_SOUL, DEFAULT_USER, DEFAULT_EXPERTISE;
|
|
3223
3252
|
var init_defaults = __esm({
|
|
3224
3253
|
"src/bootstrap/defaults.ts"() {
|
|
3225
3254
|
"use strict";
|
|
@@ -3256,6 +3285,40 @@ This file is auto-generated. Use /setup-profile to customize, or edit directly.
|
|
|
3256
3285
|
- **Timezone**: UTC
|
|
3257
3286
|
- **Communication style**: concise
|
|
3258
3287
|
- **Primary use**: general assistant
|
|
3288
|
+
`;
|
|
3289
|
+
DEFAULT_EXPERTISE = `# CC-Claw System Expertise
|
|
3290
|
+
|
|
3291
|
+
You are an expert user and operator of the CC-Claw architecture. Because you are reading this file, the user has likely asked you about a system feature, how the bot operates, its capabilities, or its internal architecture.
|
|
3292
|
+
|
|
3293
|
+
## 1. Core Architecture & Philosophy
|
|
3294
|
+
- **Multi-Backend System**: You are decoupled from any specific LLM. The user can seamlessly hot-swap your "brain" between Claude (Anthropic), Gemini (Google), and Codex (OpenAI) at any time. You maintain persistent, unified memory across all of them.
|
|
3295
|
+
- **The Daemon**: CC-Claw runs a background daemon (\`cc-claw start\`, \`cc-claw stop\`) that manages the HTTP API, Webhooks, and the Cron Scheduler.
|
|
3296
|
+
- **Database**: All state is stored locally in SQLite (\`~/.cc-claw/data/cc-claw.db\`), ensuring total privacy and fast local access for memory, jobs, and orchestration.
|
|
3297
|
+
|
|
3298
|
+
## 2. Identity & Context (The "Brain")
|
|
3299
|
+
- **SOUL & USER**: Your personality is defined in \`~/.cc-claw/workspace/SOUL.md\`, and the user's profile is in \`USER.md\`. These are the single source of truth for your behavior.
|
|
3300
|
+
- **On-Demand Context**: The \`context/\` directory holds files like this one. They are injected into your prompt only when triggered by relevant semantic keywords, keeping your context window lean.
|
|
3301
|
+
- **Proactive Memory**: You can autonomously save user facts and preferences to the database by writing \`[UPDATE_USER:key=value]\` in your replies.
|
|
3302
|
+
|
|
3303
|
+
## 3. Advanced Agent Orchestration
|
|
3304
|
+
- **Sub-Agents**: You are not limited to sequential replies. If a task requires heavy research or coding, you can spawn parallel worker agents using your \`spawn_agent\` MCP tool.
|
|
3305
|
+
- **Supervision**: You manage sub-agents by reading their messages via \`read_inbox\` and sharing data on the orchestration whiteboard via \`set_state\` / \`get_state\`.
|
|
3306
|
+
|
|
3307
|
+
## 4. Scheduling & Cron (Autonomous Execution)
|
|
3308
|
+
- **Job Management**: You manage scheduled tasks natively. Jobs can route on cron expressions, "every 4h", or "at 09:00".
|
|
3309
|
+
- **Dry Run & Webhooks**: Jobs aren't just for Telegram messages. Using \`--delivery dry_run\`, you can test jobs internally (output is logged to the database but not spammed to chat). You can also route AI output directly to external APIs using \`--delivery webhook\`.
|
|
3310
|
+
- **Auditing**: Every cron execution and its output is permanently logged in the database's \`message_log\` for verifiable auditing.
|
|
3311
|
+
|
|
3312
|
+
## 5. System Controls & Modes
|
|
3313
|
+
- **Permission Modes**: Command execution is gated. \`safe\` mode requires user approval for mutations, \`yolo\` auto-runs everything without asking, and \`plan\` is strictly read-only for drafting proposals.
|
|
3314
|
+
- **Global Settings**: The user controls your behavior system-wide using commands like \`/response_style\` (concise, normal, detailed) to enforce verbosity constraints, and \`/voice_config\` to select Text-to-Speech voices from premium providers like ElevenLabs, Grok (xAI), or local macOS.
|
|
3315
|
+
|
|
3316
|
+
## 6. Integrations & MCP
|
|
3317
|
+
- **Skills**: You can load specific workflows from the \`~/.cc-claw/workspace/skills/\` directory.
|
|
3318
|
+
- **Files**: You can send physical files to the user across Telegram by simply writing \`[SEND_FILE:/absolute/path/to/file]\` in your response.
|
|
3319
|
+
- **MCP Ecosystem**: You are deeply natively integrated with Model Context Protocol (MCP) servers (like Perplexity, NotebookLM, Context7) granting you immense external reach.
|
|
3320
|
+
|
|
3321
|
+
If the user asks *how* to do something with CC-Claw, use this expertise to suggest the most native, idiomatic approach available in the architecture.
|
|
3259
3322
|
`;
|
|
3260
3323
|
}
|
|
3261
3324
|
});
|
|
@@ -3282,6 +3345,11 @@ function bootstrapWorkspaceFiles() {
|
|
|
3282
3345
|
mkdirSync(CONTEXT_DIR, { recursive: true });
|
|
3283
3346
|
log("[bootstrap] Created context/ directory");
|
|
3284
3347
|
}
|
|
3348
|
+
const expertisePath = join3(CONTEXT_DIR, "cc-claw-expertise.md");
|
|
3349
|
+
if (!existsSync4(expertisePath)) {
|
|
3350
|
+
writeFileSync(expertisePath, DEFAULT_EXPERTISE, "utf-8");
|
|
3351
|
+
log("[bootstrap] Created default context/cc-claw-expertise.md");
|
|
3352
|
+
}
|
|
3285
3353
|
syncNativeCliFiles();
|
|
3286
3354
|
}
|
|
3287
3355
|
function syncNativeCliFiles() {
|
|
@@ -3399,12 +3467,19 @@ function searchContext(userMessage) {
|
|
|
3399
3467
|
}
|
|
3400
3468
|
return null;
|
|
3401
3469
|
}
|
|
3402
|
-
async function assembleBootstrapPrompt(userMessage, tier = "full", chatId, permMode) {
|
|
3470
|
+
async function assembleBootstrapPrompt(userMessage, tier = "full", chatId, permMode, responseStyle) {
|
|
3403
3471
|
const sections = [];
|
|
3404
3472
|
syncNativeCliFiles();
|
|
3405
3473
|
if (permMode && permMode !== "yolo") {
|
|
3406
3474
|
sections.push(buildPermissionNotice(permMode));
|
|
3407
3475
|
}
|
|
3476
|
+
if (responseStyle) {
|
|
3477
|
+
if (responseStyle === "concise") {
|
|
3478
|
+
sections.push("[Response Style]\nYou must be as concise and direct as possible. Avoid unnecessary verbosity, pleasantries, or long explanations.");
|
|
3479
|
+
} else if (responseStyle === "detailed") {
|
|
3480
|
+
sections.push("[Response Style]\nYou should be detailed and thorough in your responses. Explain concepts fully and provide comprehensive answers.");
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3408
3483
|
if (tier === "full") {
|
|
3409
3484
|
const ctx = searchContext(userMessage);
|
|
3410
3485
|
if (ctx) {
|
|
@@ -5457,7 +5532,7 @@ function startDashboard() {
|
|
|
5457
5532
|
return jsonResponse(res, { error: "message and chatId required" }, 400);
|
|
5458
5533
|
}
|
|
5459
5534
|
const { askAgent: askAgent2 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
|
|
5460
|
-
const { getMode: getMode2, getCwd: getCwd2, getModel:
|
|
5535
|
+
const { getMode: getMode2, getCwd: getCwd2, getModel: getModel3, addUsage: addUsage2, getBackend: getBackend2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
|
|
5461
5536
|
const { getAdapterForChat: getAdapterForChat2 } = await Promise.resolve().then(() => (init_backends(), backends_exports));
|
|
5462
5537
|
const chatId = body.chatId;
|
|
5463
5538
|
const PERM_LEVEL = { plan: 0, safe: 1, yolo: 2 };
|
|
@@ -5465,7 +5540,7 @@ function startDashboard() {
|
|
|
5465
5540
|
const requestedMode = body.mode ?? storedMode;
|
|
5466
5541
|
const mode = (PERM_LEVEL[requestedMode] ?? 2) <= (PERM_LEVEL[storedMode] ?? 2) ? requestedMode : storedMode;
|
|
5467
5542
|
const cwd = body.cwd ?? getCwd2(chatId);
|
|
5468
|
-
const model2 = body.model ??
|
|
5543
|
+
const model2 = body.model ?? getModel3(chatId) ?? (() => {
|
|
5469
5544
|
try {
|
|
5470
5545
|
return getAdapterForChat2(chatId).defaultModel;
|
|
5471
5546
|
} catch {
|
|
@@ -5671,7 +5746,7 @@ data: ${JSON.stringify(data)}
|
|
|
5671
5746
|
if (url.pathname === "/api/config/set" && req.method === "POST") {
|
|
5672
5747
|
try {
|
|
5673
5748
|
const body = JSON.parse(await readBody(req));
|
|
5674
|
-
const validKeys = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice"];
|
|
5749
|
+
const validKeys = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice", "response-style"];
|
|
5675
5750
|
if (!validKeys.includes(body.key)) {
|
|
5676
5751
|
return jsonResponse(res, { error: `Invalid config key. Valid: ${validKeys.join(", ")}` }, 400);
|
|
5677
5752
|
}
|
|
@@ -5689,6 +5764,9 @@ data: ${JSON.stringify(data)}
|
|
|
5689
5764
|
} else if (body.key === "voice") {
|
|
5690
5765
|
const db3 = getDb();
|
|
5691
5766
|
db3.prepare("INSERT OR REPLACE INTO chat_voice (chat_id, enabled) VALUES (?, ?)").run(body.chatId, body.value === "on" || body.value === "1" ? 1 : 0);
|
|
5767
|
+
} else if (body.key === "response-style") {
|
|
5768
|
+
const { setResponseStyle: setResponseStyle2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
|
|
5769
|
+
setResponseStyle2(body.chatId, body.value);
|
|
5692
5770
|
} else if (body.key === "backend") {
|
|
5693
5771
|
const { setBackend: setBackend2, clearSession: clearSession2, clearModel: clearModel2, clearThinkingLevel: clearThinkingLevel2 } = await Promise.resolve().then(() => (init_store4(), store_exports3));
|
|
5694
5772
|
clearSession2(body.chatId);
|
|
@@ -6116,10 +6194,11 @@ async function askAgentImpl(chatId, userMessage, opts) {
|
|
|
6116
6194
|
const { cwd, onStream, model: model2, backend: backend2, permMode, onToolAction, bootstrapTier, timeoutMs } = opts ?? {};
|
|
6117
6195
|
const adapter = backend2 ? getAdapter(backend2) : getAdapterForChat(chatId);
|
|
6118
6196
|
const mode = permMode ?? getMode(chatId);
|
|
6197
|
+
const responseStyle = getResponseStyle(chatId);
|
|
6119
6198
|
const thinkingLevel = getThinkingLevel(chatId);
|
|
6120
6199
|
const resolvedCwd = cwd ?? WORKSPACE_PATH;
|
|
6121
6200
|
const tier = bootstrapTier ?? "full";
|
|
6122
|
-
const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, chatId, mode);
|
|
6201
|
+
const fullPrompt = await assembleBootstrapPrompt(userMessage, tier, chatId, mode, responseStyle);
|
|
6123
6202
|
const existingSessionId = getSessionId(chatId);
|
|
6124
6203
|
const allowedTools = getEnabledTools(chatId);
|
|
6125
6204
|
const mcpConfigPath = getMcpConfigPath(chatId);
|
|
@@ -6280,30 +6359,40 @@ function parseTelegramTarget(target) {
|
|
|
6280
6359
|
async function deliverJobOutput(job, responseText) {
|
|
6281
6360
|
if (job.deliveryMode === "none") {
|
|
6282
6361
|
log(`[delivery] Job #${job.id}: delivery=none, skipping`);
|
|
6283
|
-
return;
|
|
6362
|
+
return true;
|
|
6363
|
+
}
|
|
6364
|
+
if (job.deliveryMode === "dry_run") {
|
|
6365
|
+
log(`[delivery] Job #${job.id}: DRY RUN \u2014 output (${responseText.length} chars):
|
|
6366
|
+
${responseText.slice(0, 500)}`);
|
|
6367
|
+
appendToLog(job.chatId, `[cron:#${job.id}:dry_run] ${job.description}`, responseText, job.backend ?? "claude", job.model, null);
|
|
6368
|
+
return true;
|
|
6284
6369
|
}
|
|
6285
6370
|
try {
|
|
6286
6371
|
if (job.deliveryMode === "webhook") {
|
|
6287
6372
|
await deliverWebhook(job, responseText);
|
|
6288
|
-
|
|
6373
|
+
appendToLog(job.chatId, `[cron:#${job.id}] ${job.description}`, responseText, job.backend ?? "claude", job.model, null);
|
|
6374
|
+
return true;
|
|
6289
6375
|
}
|
|
6290
6376
|
const channelName = job.channel ?? "telegram";
|
|
6291
6377
|
const channel = registry?.get(channelName);
|
|
6292
6378
|
if (!channel) {
|
|
6293
6379
|
error(`[delivery] Job #${job.id}: channel "${channelName}" not found`);
|
|
6294
|
-
return;
|
|
6380
|
+
return false;
|
|
6295
6381
|
}
|
|
6296
6382
|
const targetChatId = job.target ?? job.chatId;
|
|
6297
6383
|
const cleanText = await processFileSends(targetChatId, channel, responseText);
|
|
6298
|
-
if (!cleanText) return;
|
|
6384
|
+
if (!cleanText) return true;
|
|
6299
6385
|
if (channelName === "telegram") {
|
|
6300
6386
|
const parsed = parseTelegramTarget(targetChatId);
|
|
6301
6387
|
await channel.sendText(parsed.chatId, cleanText);
|
|
6302
6388
|
} else {
|
|
6303
6389
|
await channel.sendText(targetChatId, cleanText);
|
|
6304
6390
|
}
|
|
6391
|
+
appendToLog(job.chatId, `[cron:#${job.id}] ${job.description}`, cleanText, job.backend ?? "claude", job.model, null);
|
|
6392
|
+
return true;
|
|
6305
6393
|
} catch (err) {
|
|
6306
6394
|
error(`[delivery] Job #${job.id} delivery failed (non-fatal): ${errorMessage(err)}`);
|
|
6395
|
+
return false;
|
|
6307
6396
|
}
|
|
6308
6397
|
}
|
|
6309
6398
|
async function deliverWebhook(job, responseText) {
|
|
@@ -6362,6 +6451,7 @@ var init_delivery = __esm({
|
|
|
6362
6451
|
"src/scheduler/delivery.ts"() {
|
|
6363
6452
|
"use strict";
|
|
6364
6453
|
init_log();
|
|
6454
|
+
init_session_log();
|
|
6365
6455
|
registry = null;
|
|
6366
6456
|
BLOCKED_PATH_PATTERNS = [
|
|
6367
6457
|
/\/\.ssh\//,
|
|
@@ -6691,18 +6781,22 @@ async function executeJob(job) {
|
|
|
6691
6781
|
return;
|
|
6692
6782
|
}
|
|
6693
6783
|
const response = await runWithRetry(job, resolvedModel, runId, t0);
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
cacheRead: response.usage?.cacheRead,
|
|
6698
|
-
durationMs: Date.now() - t0
|
|
6699
|
-
});
|
|
6784
|
+
const isEmpty = !response.text || response.text.trim().length === 0 || /^\(No response from/i.test(response.text);
|
|
6785
|
+
const contentStatus = isEmpty ? "no_content" : "success";
|
|
6786
|
+
const durationMs = Date.now() - t0;
|
|
6700
6787
|
updateJobLastRun(job.id, (/* @__PURE__ */ new Date()).toISOString());
|
|
6701
6788
|
resetJobFailures(job.id);
|
|
6702
6789
|
if (response.usage) {
|
|
6703
6790
|
addUsage(job.chatId, response.usage.input, response.usage.output, response.usage.cacheRead, resolvedModel);
|
|
6704
6791
|
}
|
|
6705
|
-
await deliverJobOutput(job, response.text);
|
|
6792
|
+
const delivered = await deliverJobOutput(job, response.text);
|
|
6793
|
+
const finalStatus = !delivered && contentStatus === "success" ? "delivery_failed" : contentStatus;
|
|
6794
|
+
completeJobRun(runId, finalStatus, {
|
|
6795
|
+
usageInput: response.usage?.input,
|
|
6796
|
+
usageOutput: response.usage?.output,
|
|
6797
|
+
cacheRead: response.usage?.cacheRead,
|
|
6798
|
+
durationMs
|
|
6799
|
+
});
|
|
6706
6800
|
} catch (err) {
|
|
6707
6801
|
const durationMs = Date.now() - t0;
|
|
6708
6802
|
const errorClass = classifyError(err);
|
|
@@ -6788,7 +6882,7 @@ function resolveJobModel(job) {
|
|
|
6788
6882
|
const backendId = resolveJobBackendId(job);
|
|
6789
6883
|
try {
|
|
6790
6884
|
const adapter = getAdapter(backendId);
|
|
6791
|
-
return
|
|
6885
|
+
return adapter.defaultModel;
|
|
6792
6886
|
} catch {
|
|
6793
6887
|
return "claude-sonnet-4-6";
|
|
6794
6888
|
}
|
|
@@ -7445,6 +7539,8 @@ var init_telegram2 = __esm({
|
|
|
7445
7539
|
// Skills & profile
|
|
7446
7540
|
{ command: "skills", description: "List and invoke skills" },
|
|
7447
7541
|
{ command: "voice", description: "Toggle voice responses" },
|
|
7542
|
+
{ command: "voice_config", description: "Configure voice provider and voice" },
|
|
7543
|
+
{ command: "response_style", description: "Set the AI response style (concise/normal/detailed)" },
|
|
7448
7544
|
{ command: "imagine", description: "Generate an image from a prompt" },
|
|
7449
7545
|
{ command: "heartbeat", description: "Configure proactive heartbeat" },
|
|
7450
7546
|
{ command: "chats", description: "Manage multi-chat aliases" }
|
|
@@ -8458,6 +8554,23 @@ function toggleVoice(chatId) {
|
|
|
8458
8554
|
`).run(chatId, newState, newState);
|
|
8459
8555
|
return newState === 1;
|
|
8460
8556
|
}
|
|
8557
|
+
function getVoiceConfig(chatId) {
|
|
8558
|
+
const db3 = getDb();
|
|
8559
|
+
const row = db3.prepare("SELECT enabled, provider, voice_id FROM chat_voice WHERE chat_id = ?").get(chatId);
|
|
8560
|
+
return {
|
|
8561
|
+
enabled: row?.enabled === 1,
|
|
8562
|
+
provider: row?.provider ?? "elevenlabs",
|
|
8563
|
+
voiceId: row?.voice_id ?? null
|
|
8564
|
+
};
|
|
8565
|
+
}
|
|
8566
|
+
function setVoiceProvider(chatId, provider, voiceId) {
|
|
8567
|
+
const db3 = getDb();
|
|
8568
|
+
db3.prepare(`
|
|
8569
|
+
INSERT INTO chat_voice (chat_id, enabled, provider, voice_id)
|
|
8570
|
+
VALUES (?, 1, ?, ?)
|
|
8571
|
+
ON CONFLICT(chat_id) DO UPDATE SET provider = ?, voice_id = ?, enabled = 1
|
|
8572
|
+
`).run(chatId, provider, voiceId, provider, voiceId);
|
|
8573
|
+
}
|
|
8461
8574
|
async function transcribeAudio(audioBuffer, mimeType = "audio/ogg") {
|
|
8462
8575
|
const GROQ_API_KEY = process.env.GROQ_API_KEY;
|
|
8463
8576
|
if (!GROQ_API_KEY) return null;
|
|
@@ -8476,25 +8589,48 @@ async function transcribeAudio(audioBuffer, mimeType = "audio/ogg") {
|
|
|
8476
8589
|
}
|
|
8477
8590
|
return (await response.text()).trim();
|
|
8478
8591
|
}
|
|
8479
|
-
async function synthesizeSpeech(text) {
|
|
8592
|
+
async function synthesizeSpeech(text, chatId) {
|
|
8480
8593
|
const ttsText = text.length > 4e3 ? text.slice(0, 4e3) + "..." : text;
|
|
8481
|
-
const
|
|
8482
|
-
|
|
8594
|
+
const config2 = chatId ? getVoiceConfig(chatId) : null;
|
|
8595
|
+
const provider = config2?.provider ?? "elevenlabs";
|
|
8596
|
+
if (provider === "grok") {
|
|
8597
|
+
const xaiKey = process.env.XAI_API_KEY;
|
|
8598
|
+
if (xaiKey) {
|
|
8599
|
+
try {
|
|
8600
|
+
return await grokTts(ttsText, xaiKey, config2?.voiceId ?? "eve");
|
|
8601
|
+
} catch (err) {
|
|
8602
|
+
error("[tts] Grok failed:", err);
|
|
8603
|
+
}
|
|
8604
|
+
} else {
|
|
8605
|
+
log("[tts] Grok selected but XAI_API_KEY not set, falling back");
|
|
8606
|
+
}
|
|
8607
|
+
}
|
|
8608
|
+
if (provider === "macos") {
|
|
8483
8609
|
try {
|
|
8484
|
-
return await
|
|
8610
|
+
return await macOsTts(ttsText, config2?.voiceId ?? "Samantha");
|
|
8485
8611
|
} catch (err) {
|
|
8486
|
-
error("[tts]
|
|
8612
|
+
error("[tts] macOS TTS failed:", err);
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
if (provider === "elevenlabs" || provider === "grok") {
|
|
8616
|
+
const elevenLabsKey = process.env.ELEVENLABS_API_KEY;
|
|
8617
|
+
if (elevenLabsKey) {
|
|
8618
|
+
try {
|
|
8619
|
+
const voiceId = provider === "elevenlabs" && config2?.voiceId ? config2.voiceId : process.env.ELEVENLABS_VOICE_ID ?? "21m00Tcm4TlvDq8ikWAM";
|
|
8620
|
+
return await elevenLabsTts(ttsText, elevenLabsKey, voiceId);
|
|
8621
|
+
} catch (err) {
|
|
8622
|
+
error("[tts] ElevenLabs failed:", err);
|
|
8623
|
+
}
|
|
8487
8624
|
}
|
|
8488
8625
|
}
|
|
8489
8626
|
try {
|
|
8490
|
-
return await macOsTts(ttsText);
|
|
8627
|
+
return await macOsTts(ttsText, "Samantha");
|
|
8491
8628
|
} catch (err) {
|
|
8492
8629
|
error("[tts] macOS TTS failed:", err);
|
|
8493
8630
|
}
|
|
8494
8631
|
return null;
|
|
8495
8632
|
}
|
|
8496
|
-
async function elevenLabsTts(text, apiKey) {
|
|
8497
|
-
const voiceId = process.env.ELEVENLABS_VOICE_ID ?? "21m00Tcm4TlvDq8ikWAM";
|
|
8633
|
+
async function elevenLabsTts(text, apiKey, voiceId) {
|
|
8498
8634
|
const response = await fetch(
|
|
8499
8635
|
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
|
|
8500
8636
|
{
|
|
@@ -8515,12 +8651,48 @@ async function elevenLabsTts(text, apiKey) {
|
|
|
8515
8651
|
}
|
|
8516
8652
|
return Buffer.from(await response.arrayBuffer());
|
|
8517
8653
|
}
|
|
8518
|
-
async function
|
|
8654
|
+
async function grokTts(text, apiKey, voiceId) {
|
|
8655
|
+
const response = await fetch("https://api.x.ai/v1/tts", {
|
|
8656
|
+
method: "POST",
|
|
8657
|
+
headers: {
|
|
8658
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
8659
|
+
"Content-Type": "application/json"
|
|
8660
|
+
},
|
|
8661
|
+
body: JSON.stringify({
|
|
8662
|
+
text,
|
|
8663
|
+
voice_id: voiceId,
|
|
8664
|
+
language: "en"
|
|
8665
|
+
})
|
|
8666
|
+
});
|
|
8667
|
+
if (!response.ok) {
|
|
8668
|
+
const errText = await response.text().catch(() => "");
|
|
8669
|
+
throw new Error(`Grok TTS API error: ${response.status} ${errText}`);
|
|
8670
|
+
}
|
|
8671
|
+
const mp3Buffer = Buffer.from(await response.arrayBuffer());
|
|
8672
|
+
return await mp3ToOgg(mp3Buffer);
|
|
8673
|
+
}
|
|
8674
|
+
async function mp3ToOgg(mp3Buffer) {
|
|
8675
|
+
const id = crypto.randomUUID();
|
|
8676
|
+
const tmpMp3 = `/tmp/cc-claw-tts-${id}.mp3`;
|
|
8677
|
+
const tmpOgg = `/tmp/cc-claw-tts-${id}.ogg`;
|
|
8678
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
8679
|
+
await writeFile4(tmpMp3, mp3Buffer);
|
|
8680
|
+
await execFileAsync2("ffmpeg", ["-y", "-i", tmpMp3, "-c:a", "libopus", "-b:a", "64k", tmpOgg]);
|
|
8681
|
+
const oggBuffer = await readFile4(tmpOgg);
|
|
8682
|
+
unlink(tmpMp3).catch((err) => {
|
|
8683
|
+
error("[tts] cleanup failed:", err);
|
|
8684
|
+
});
|
|
8685
|
+
unlink(tmpOgg).catch((err) => {
|
|
8686
|
+
error("[tts] cleanup failed:", err);
|
|
8687
|
+
});
|
|
8688
|
+
return oggBuffer;
|
|
8689
|
+
}
|
|
8690
|
+
async function macOsTts(text, voice2 = "Samantha") {
|
|
8519
8691
|
const id = crypto.randomUUID();
|
|
8520
8692
|
const tmpAiff = `/tmp/cc-claw-tts-${id}.aiff`;
|
|
8521
8693
|
const tmpOgg = `/tmp/cc-claw-tts-${id}.ogg`;
|
|
8522
8694
|
const sanitized = text.replace(/['"\\$`]/g, " ");
|
|
8523
|
-
await execFileAsync2("say", ["-o", tmpAiff, sanitized]);
|
|
8695
|
+
await execFileAsync2("say", ["-v", voice2, "-o", tmpAiff, sanitized]);
|
|
8524
8696
|
await execFileAsync2("ffmpeg", ["-y", "-i", tmpAiff, "-c:a", "libopus", "-b:a", "64k", tmpOgg]);
|
|
8525
8697
|
const oggBuffer = await readFile4(tmpOgg);
|
|
8526
8698
|
unlink(tmpAiff).catch((err) => {
|
|
@@ -8531,13 +8703,28 @@ async function macOsTts(text) {
|
|
|
8531
8703
|
});
|
|
8532
8704
|
return oggBuffer;
|
|
8533
8705
|
}
|
|
8534
|
-
var execFileAsync2;
|
|
8706
|
+
var execFileAsync2, ELEVENLABS_VOICES, GROK_VOICES, MACOS_VOICES;
|
|
8535
8707
|
var init_stt = __esm({
|
|
8536
8708
|
"src/voice/stt.ts"() {
|
|
8537
8709
|
"use strict";
|
|
8538
8710
|
init_log();
|
|
8539
8711
|
init_store4();
|
|
8540
8712
|
execFileAsync2 = promisify2(execFile2);
|
|
8713
|
+
ELEVENLABS_VOICES = {
|
|
8714
|
+
"21m00Tcm4TlvDq8ikWAM": { name: "Rachel", gender: "F" },
|
|
8715
|
+
"EXAVITQu4vr4xnSDxMaL": { name: "Sarah", gender: "F" },
|
|
8716
|
+
"XB0fDUnXU5powFXDhCwa": { name: "Charlotte", gender: "F" },
|
|
8717
|
+
"pFZP5JQG7iQjIQuC4Bku": { name: "Lily", gender: "F" },
|
|
8718
|
+
"pNInz6obpgDQGcFmaJgB": { name: "Adam", gender: "M" },
|
|
8719
|
+
"nPczCjzI2devNBz1zQrb": { name: "Brian", gender: "M" },
|
|
8720
|
+
"onwK4e9ZLuTAKqWW03F9": { name: "Daniel", gender: "M" },
|
|
8721
|
+
"TxGEqnHWrfWFTfGW9XjX": { name: "Josh", gender: "M" }
|
|
8722
|
+
};
|
|
8723
|
+
GROK_VOICES = ["eve", "ara", "rex", "sal", "leo"];
|
|
8724
|
+
MACOS_VOICES = {
|
|
8725
|
+
"Samantha": { name: "Samantha", gender: "F" },
|
|
8726
|
+
"Albert": { name: "Albert", gender: "M" }
|
|
8727
|
+
};
|
|
8541
8728
|
}
|
|
8542
8729
|
});
|
|
8543
8730
|
|
|
@@ -9506,7 +9693,7 @@ async function handleCommand(msg, channel) {
|
|
|
9506
9693
|
case "help":
|
|
9507
9694
|
await channel.sendText(
|
|
9508
9695
|
chatId,
|
|
9509
|
-
"Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/summarize - Save session to memory (without resetting)\n/summarize all - Summarize all pending sessions (pre-restart)\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/remember <text> - Save a memory\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/imagine <prompt> - Generate an image (or /image)\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/agents - List active sub-agents\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/help - Show this message",
|
|
9696
|
+
"Hey! I'm CC-Claw \u2014 your personal AI assistant on Telegram.\n\nI use AI coding CLIs (Claude, Gemini, Codex) as my brain. Just send me a message to get started.\n\nCommands:\n/backend [name] - Switch AI backend (or /claude /gemini /codex)\n/model - Switch model for active backend\n/summarizer - Configure session summarization model\n/status - Show session, model, backend, and usage\n/cost - Show estimated API cost (use /cost all for all-time)\n/usage - Show usage per backend with limits\n/limits - Configure usage limits per backend\n/newchat - Start a fresh conversation\n/summarize - Save session to memory (without resetting)\n/summarize all - Summarize all pending sessions (pre-restart)\n/cwd <path> - Set working directory\n/cwd - Show current working directory\n/memory - List stored memories\n/remember <text> - Save a memory\n/forget <keyword> - Remove a memory\n/voice - Toggle voice responses\n/voice_config - Configure voice provider and voice\n/imagine <prompt> - Generate an image (or /image)\n/cron <description> - Schedule a task (or /schedule)\n/cron - List scheduled jobs (or /jobs)\n/cron cancel <id> - Cancel a job\n/cron pause <id> - Pause a job\n/cron resume <id> - Resume a job\n/cron run <id> - Trigger a job now\n/cron runs [id] - View run history\n/cron edit <id> - Edit a job\n/cron health - Scheduler health\n/skills - List skills from all backends\n/skill-install <url> - Install a skill from GitHub\n/setup-profile - Set up your user profile\n/chats - List authorized chats and aliases\n/heartbeat - Proactive awareness (on/off/interval/hours)\n/history - List recent session summaries\n/stop - Cancel the current running task\n/tools - Configure which tools the agent can use\n/permissions - Switch permission mode (yolo/safe/plan)\n/verbose - Tool visibility (off/normal/verbose)\n/agents - List active sub-agents\n/tasks - Show task board for current orchestration\n/stopagent <id> - Cancel a specific sub-agent\n/stopall - Cancel all sub-agents in this chat\n/runners - List registered CLI runners\n/mcps - List registered MCP servers\n/help - Show this message",
|
|
9510
9697
|
"plain"
|
|
9511
9698
|
);
|
|
9512
9699
|
break;
|
|
@@ -10000,12 +10187,37 @@ ${lines.join("\n")}`, "plain");
|
|
|
10000
10187
|
break;
|
|
10001
10188
|
}
|
|
10002
10189
|
case "voice": {
|
|
10003
|
-
const
|
|
10004
|
-
|
|
10005
|
-
chatId,
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10190
|
+
const vcEnabled = isVoiceEnabled(chatId);
|
|
10191
|
+
if (typeof channel.sendKeyboard === "function") {
|
|
10192
|
+
await channel.sendKeyboard(chatId, `\u{1F3A7} Voice responses: ${vcEnabled ? "ON" : "OFF"}`, [
|
|
10193
|
+
[
|
|
10194
|
+
{ label: `${vcEnabled ? "" : "\u2713 "}\u{1F507} Off`, data: "voice:off" },
|
|
10195
|
+
{ label: `${vcEnabled ? "\u2713 " : ""}\u{1F50A} On`, data: "voice:on" }
|
|
10196
|
+
]
|
|
10197
|
+
]);
|
|
10198
|
+
} else {
|
|
10199
|
+
const toggled = toggleVoice(chatId);
|
|
10200
|
+
await channel.sendText(chatId, toggled ? "Voice responses enabled." : "Voice responses disabled.", "plain");
|
|
10201
|
+
}
|
|
10202
|
+
break;
|
|
10203
|
+
}
|
|
10204
|
+
case "voice_config": {
|
|
10205
|
+
await sendVoiceConfigKeyboard(chatId, channel);
|
|
10206
|
+
break;
|
|
10207
|
+
}
|
|
10208
|
+
case "response_style": {
|
|
10209
|
+
const currentStyle = getResponseStyle(chatId);
|
|
10210
|
+
if (typeof channel.sendKeyboard === "function") {
|
|
10211
|
+
await channel.sendKeyboard(chatId, "\u{1F5E3}\uFE0F AI Response Style:", [
|
|
10212
|
+
[
|
|
10213
|
+
{ label: `${currentStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise" },
|
|
10214
|
+
{ label: `${currentStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal" },
|
|
10215
|
+
{ label: `${currentStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed" }
|
|
10216
|
+
]
|
|
10217
|
+
]);
|
|
10218
|
+
} else {
|
|
10219
|
+
await channel.sendText(chatId, `Current Response Style: ${currentStyle}`, "plain");
|
|
10220
|
+
}
|
|
10009
10221
|
break;
|
|
10010
10222
|
}
|
|
10011
10223
|
case "imagine":
|
|
@@ -10697,24 +10909,42 @@ async function handleMedia(msg, channel) {
|
|
|
10697
10909
|
return;
|
|
10698
10910
|
}
|
|
10699
10911
|
const videoBuffer = await channel.downloadFile(fileId);
|
|
10700
|
-
const
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
10912
|
+
const videoMime = msg.metadata?.mimeType ?? msg.mimeType ?? "video/mp4";
|
|
10913
|
+
const videoExt = videoMime.split("/")[1]?.replace("quicktime", "mov") || "mp4";
|
|
10914
|
+
const tempVideoPath = `/tmp/cc-claw-video-${Date.now()}.${videoExt}`;
|
|
10915
|
+
await writeFile2(tempVideoPath, videoBuffer);
|
|
10916
|
+
let prompt2;
|
|
10917
|
+
if (wantsVideoAnalysis(caption)) {
|
|
10918
|
+
const geminiPrompt = caption || "Analyze this video and describe what you see in detail.";
|
|
10919
|
+
let analysis;
|
|
10920
|
+
try {
|
|
10921
|
+
analysis = await analyzeVideo(videoBuffer, geminiPrompt, videoMime);
|
|
10922
|
+
} catch (err) {
|
|
10923
|
+
analysis = `Video analysis failed: ${errorMessage(err)}`;
|
|
10924
|
+
}
|
|
10925
|
+
prompt2 = caption ? `The user sent a video with caption: "${caption}"
|
|
10709
10926
|
|
|
10710
10927
|
Gemini video analysis:
|
|
10711
10928
|
${analysis}
|
|
10712
10929
|
|
|
10930
|
+
The video is also saved at: ${tempVideoPath}
|
|
10931
|
+
|
|
10713
10932
|
Respond to the user based on this analysis.` : `The user sent a video. Gemini analyzed it:
|
|
10714
10933
|
|
|
10715
10934
|
${analysis}
|
|
10716
10935
|
|
|
10936
|
+
The video is also saved at: ${tempVideoPath}
|
|
10937
|
+
|
|
10717
10938
|
Summarize this for the user.`;
|
|
10939
|
+
} else {
|
|
10940
|
+
prompt2 = caption ? `The user sent a video with caption: "${caption}"
|
|
10941
|
+
|
|
10942
|
+
The video has been saved to: ${tempVideoPath}
|
|
10943
|
+
|
|
10944
|
+
Respond to their message. Do NOT analyze the video unless they ask you to.` : `The user sent a video. It has been saved to: ${tempVideoPath}
|
|
10945
|
+
|
|
10946
|
+
Acknowledge receipt. Do NOT analyze the video unless they ask you to.`;
|
|
10947
|
+
}
|
|
10718
10948
|
const vidModel = resolveModel(chatId);
|
|
10719
10949
|
const vMode = getMode(chatId);
|
|
10720
10950
|
const vidVerbose = getVerboseLevel(chatId);
|
|
@@ -11068,7 +11298,7 @@ async function sendResponse(chatId, channel, text, messageId) {
|
|
|
11068
11298
|
if (!cleanText) return;
|
|
11069
11299
|
if (isVoiceEnabled(chatId)) {
|
|
11070
11300
|
try {
|
|
11071
|
-
const audioBuffer = await synthesizeSpeech(cleanText);
|
|
11301
|
+
const audioBuffer = await synthesizeSpeech(cleanText, chatId);
|
|
11072
11302
|
if (audioBuffer) {
|
|
11073
11303
|
await channel.sendVoice(chatId, audioBuffer);
|
|
11074
11304
|
return;
|
|
@@ -11082,6 +11312,70 @@ async function sendResponse(chatId, channel, text, messageId) {
|
|
|
11082
11312
|
function isImageExt(ext) {
|
|
11083
11313
|
return ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"].includes(ext);
|
|
11084
11314
|
}
|
|
11315
|
+
async function sendVoiceConfigKeyboard(chatId, channel) {
|
|
11316
|
+
if (typeof channel.sendKeyboard !== "function") {
|
|
11317
|
+
await channel.sendText(chatId, "Voice configuration requires an interactive channel (Telegram).", "plain");
|
|
11318
|
+
return;
|
|
11319
|
+
}
|
|
11320
|
+
const config2 = getVoiceConfig(chatId);
|
|
11321
|
+
const currentVoiceName = config2.provider === "elevenlabs" ? ELEVENLABS_VOICES[config2.voiceId ?? ""]?.name ?? "Rachel" : config2.provider === "macos" ? MACOS_VOICES[config2.voiceId ?? ""]?.name ?? "Samantha" : config2.voiceId ?? "eve";
|
|
11322
|
+
const providerLabel = config2.provider === "grok" ? "Grok (xAI)" : config2.provider === "macos" ? "macOS" : "ElevenLabs";
|
|
11323
|
+
const header2 = `\u{1F3A7} Voice Configuration
|
|
11324
|
+
Provider: ${providerLabel}
|
|
11325
|
+
Voice: ${currentVoiceName}
|
|
11326
|
+
Status: ${config2.enabled ? "ON" : "OFF"}`;
|
|
11327
|
+
const buttons = [];
|
|
11328
|
+
buttons.push([
|
|
11329
|
+
{ label: `${config2.provider === "elevenlabs" ? "\u2713 " : ""}ElevenLabs`, data: "vcfg:p:elevenlabs" },
|
|
11330
|
+
{ label: `${config2.provider === "grok" ? "\u2713 " : ""}Grok`, data: "vcfg:p:grok" },
|
|
11331
|
+
{ label: `${config2.provider === "macos" ? "\u2713 " : ""}macOS`, data: "vcfg:p:macos" }
|
|
11332
|
+
]);
|
|
11333
|
+
if (config2.provider === "elevenlabs") {
|
|
11334
|
+
const entries = Object.entries(ELEVENLABS_VOICES);
|
|
11335
|
+
const female = entries.filter(([, v]) => v.gender === "F");
|
|
11336
|
+
const male = entries.filter(([, v]) => v.gender === "M");
|
|
11337
|
+
buttons.push(female.map(([id, v]) => ({
|
|
11338
|
+
label: `${config2.voiceId === id ? "\u2713 " : ""}${v.name}`,
|
|
11339
|
+
data: `vcfg:v:${id}`
|
|
11340
|
+
})));
|
|
11341
|
+
buttons.push(male.map(([id, v]) => ({
|
|
11342
|
+
label: `${config2.voiceId === id ? "\u2713 " : ""}${v.name}`,
|
|
11343
|
+
data: `vcfg:v:${id}`
|
|
11344
|
+
})));
|
|
11345
|
+
} else if (config2.provider === "grok") {
|
|
11346
|
+
buttons.push(GROK_VOICES.map((v) => ({
|
|
11347
|
+
label: `${config2.voiceId === v ? "\u2713 " : ""}${v.charAt(0).toUpperCase() + v.slice(1)}`,
|
|
11348
|
+
data: `vcfg:v:${v}`
|
|
11349
|
+
})));
|
|
11350
|
+
} else {
|
|
11351
|
+
const entries = Object.entries(MACOS_VOICES);
|
|
11352
|
+
buttons.push(entries.map(([id, v]) => ({
|
|
11353
|
+
label: `${config2.voiceId === id ? "\u2713 " : ""}${v.name}`,
|
|
11354
|
+
data: `vcfg:v:${id}`
|
|
11355
|
+
})));
|
|
11356
|
+
}
|
|
11357
|
+
await channel.sendKeyboard(chatId, header2, buttons);
|
|
11358
|
+
}
|
|
11359
|
+
function wantsVideoAnalysis(caption) {
|
|
11360
|
+
if (!caption) return false;
|
|
11361
|
+
const lower = caption.toLowerCase();
|
|
11362
|
+
const patterns = [
|
|
11363
|
+
/\banalyze\b/,
|
|
11364
|
+
/\banalysis\b/,
|
|
11365
|
+
/\bdescribe\b/,
|
|
11366
|
+
/\bwhat('s| is) (in |happening)/,
|
|
11367
|
+
/\btell me (about|what)/,
|
|
11368
|
+
/\bexplain\b/,
|
|
11369
|
+
/\bsummarize\b/,
|
|
11370
|
+
/\bwhat do you see\b/,
|
|
11371
|
+
/\breview\b/,
|
|
11372
|
+
/\bwatch\b/,
|
|
11373
|
+
/\btranscri/,
|
|
11374
|
+
/\bidentif/,
|
|
11375
|
+
/\bwhat happens\b/
|
|
11376
|
+
];
|
|
11377
|
+
return patterns.some((p) => p.test(lower));
|
|
11378
|
+
}
|
|
11085
11379
|
async function sendBackendSwitchConfirmation(chatId, target, channel) {
|
|
11086
11380
|
const current = getBackend(chatId);
|
|
11087
11381
|
const targetAdapter = getAdapter(target);
|
|
@@ -11315,8 +11609,9 @@ ${PERM_MODES[chosen]}`,
|
|
|
11315
11609
|
pendingInterrupts.delete(targetChatId);
|
|
11316
11610
|
stopAgent(targetChatId);
|
|
11317
11611
|
await channel.sendText(chatId, "\u26A1 Stopping current task and processing your message\u2026", "plain");
|
|
11318
|
-
|
|
11319
|
-
|
|
11612
|
+
bypassBusyCheck.add(targetChatId);
|
|
11613
|
+
handleMessage(pending.msg, pending.channel).catch(() => {
|
|
11614
|
+
});
|
|
11320
11615
|
} else if (action === "queue" && pending) {
|
|
11321
11616
|
pendingInterrupts.delete(targetChatId);
|
|
11322
11617
|
bypassBusyCheck.add(targetChatId);
|
|
@@ -11347,6 +11642,60 @@ ${PERM_MODES[chosen]}`,
|
|
|
11347
11642
|
} else {
|
|
11348
11643
|
await channel.sendText(chatId, "Fallback expired. Use /backend to switch manually.", "plain");
|
|
11349
11644
|
}
|
|
11645
|
+
} else if (data.startsWith("voice:")) {
|
|
11646
|
+
const action = data.slice(6);
|
|
11647
|
+
if (action === "on" || action === "off") {
|
|
11648
|
+
const current = isVoiceEnabled(chatId);
|
|
11649
|
+
const desired = action === "on";
|
|
11650
|
+
if (current !== desired) toggleVoice(chatId);
|
|
11651
|
+
await channel.sendText(chatId, desired ? "\u{1F50A} Voice responses enabled." : "\u{1F507} Voice responses disabled.", "plain");
|
|
11652
|
+
}
|
|
11653
|
+
} else if (data.startsWith("style:")) {
|
|
11654
|
+
const selectedStyle = data.split(":")[1];
|
|
11655
|
+
if (selectedStyle === "concise" || selectedStyle === "normal" || selectedStyle === "detailed") {
|
|
11656
|
+
setResponseStyle(chatId, selectedStyle);
|
|
11657
|
+
if (typeof channel.sendKeyboard === "function") {
|
|
11658
|
+
await channel.sendKeyboard(chatId, "\u{1F5E3}\uFE0F AI Response Style:", [
|
|
11659
|
+
[
|
|
11660
|
+
{ label: `${selectedStyle === "concise" ? "\u2713 " : ""}Concise`, data: "style:concise" },
|
|
11661
|
+
{ label: `${selectedStyle === "normal" ? "\u2713 " : ""}Normal`, data: "style:normal" },
|
|
11662
|
+
{ label: `${selectedStyle === "detailed" ? "\u2713 " : ""}Detailed`, data: "style:detailed" }
|
|
11663
|
+
]
|
|
11664
|
+
]);
|
|
11665
|
+
}
|
|
11666
|
+
await channel.sendText(chatId, `Response style set to: ${selectedStyle}`, "plain");
|
|
11667
|
+
}
|
|
11668
|
+
} else if (data.startsWith("vcfg:")) {
|
|
11669
|
+
const parts = data.slice(5).split(":");
|
|
11670
|
+
const action = parts[0];
|
|
11671
|
+
if (action === "p") {
|
|
11672
|
+
const provider = parts[1];
|
|
11673
|
+
if (provider === "elevenlabs" || provider === "grok" || provider === "macos") {
|
|
11674
|
+
if (provider === "grok" && !process.env.XAI_API_KEY) {
|
|
11675
|
+
await channel.sendText(
|
|
11676
|
+
chatId,
|
|
11677
|
+
"\u26A0\uFE0F Grok requires `XAI_API_KEY` to be set.\n\nAdd it to your config:\n```\necho 'XAI_API_KEY=your-key-here' >> ~/.cc-claw/.env\ncc-claw service restart\n```\n\nGet a key at: https://console.x.ai/team/default/api-keys\n\nSetting Grok as your provider \u2014 it will activate once the key is added.",
|
|
11678
|
+
"markdown"
|
|
11679
|
+
);
|
|
11680
|
+
}
|
|
11681
|
+
if (provider === "elevenlabs" && !process.env.ELEVENLABS_API_KEY) {
|
|
11682
|
+
await channel.sendText(
|
|
11683
|
+
chatId,
|
|
11684
|
+
"\u26A0\uFE0F ElevenLabs requires `ELEVENLABS_API_KEY` to be set.\n\nAdd it to your config:\n```\necho 'ELEVENLABS_API_KEY=your-key-here' >> ~/.cc-claw/.env\ncc-claw service restart\n```\n\nGet a key at: https://elevenlabs.io/api\n\nSetting ElevenLabs as your provider \u2014 it will activate once the key is added.",
|
|
11685
|
+
"markdown"
|
|
11686
|
+
);
|
|
11687
|
+
}
|
|
11688
|
+
const defaultVoice = provider === "grok" ? "eve" : provider === "macos" ? "Samantha" : "21m00Tcm4TlvDq8ikWAM";
|
|
11689
|
+
setVoiceProvider(chatId, provider, defaultVoice);
|
|
11690
|
+
await sendVoiceConfigKeyboard(chatId, channel);
|
|
11691
|
+
}
|
|
11692
|
+
} else if (action === "v") {
|
|
11693
|
+
const voiceId = parts.slice(1).join(":");
|
|
11694
|
+
const config2 = getVoiceConfig(chatId);
|
|
11695
|
+
setVoiceProvider(chatId, config2.provider, voiceId);
|
|
11696
|
+
const voiceName = config2.provider === "elevenlabs" ? ELEVENLABS_VOICES[voiceId]?.name ?? voiceId : config2.provider === "macos" ? MACOS_VOICES[voiceId]?.name ?? voiceId : voiceId;
|
|
11697
|
+
await channel.sendText(chatId, `\u2705 Voice set to: ${voiceName}`, "plain");
|
|
11698
|
+
}
|
|
11350
11699
|
} else if (data.startsWith("skills:page:")) {
|
|
11351
11700
|
const page = parseInt(data.slice(12), 10);
|
|
11352
11701
|
const skills2 = await discoverAllSkills();
|
|
@@ -12975,7 +13324,7 @@ async function apiGet(path) {
|
|
|
12975
13324
|
const req = httpRequest(url, {
|
|
12976
13325
|
method: "GET",
|
|
12977
13326
|
headers: token ? { "Authorization": `Bearer ${token}` } : {},
|
|
12978
|
-
timeout:
|
|
13327
|
+
timeout: 3e3
|
|
12979
13328
|
}, (res) => {
|
|
12980
13329
|
const chunks = [];
|
|
12981
13330
|
res.on("data", (c) => chunks.push(c));
|
|
@@ -14533,7 +14882,7 @@ var init_config = __esm({
|
|
|
14533
14882
|
init_format();
|
|
14534
14883
|
init_paths();
|
|
14535
14884
|
init_resolve_chat();
|
|
14536
|
-
RUNTIME_KEYS = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice"];
|
|
14885
|
+
RUNTIME_KEYS = ["backend", "model", "thinking", "summarizer", "mode", "verbose", "cwd", "voice", "response-style"];
|
|
14537
14886
|
KEY_TABLE_MAP = {
|
|
14538
14887
|
backend: { table: "chat_backend", col: "backend" },
|
|
14539
14888
|
model: { table: "chat_model", col: "model" },
|
|
@@ -14542,7 +14891,8 @@ var init_config = __esm({
|
|
|
14542
14891
|
mode: { table: "chat_mode", col: "mode" },
|
|
14543
14892
|
verbose: { table: "chat_verbose", col: "level" },
|
|
14544
14893
|
cwd: { table: "chat_cwd", col: "cwd" },
|
|
14545
|
-
voice: { table: "chat_voice", col: "enabled" }
|
|
14894
|
+
voice: { table: "chat_voice", col: "enabled" },
|
|
14895
|
+
"response-style": { table: "chat_response_style", col: "style" }
|
|
14546
14896
|
};
|
|
14547
14897
|
ALLOWED_TABLES = new Set(Object.values(KEY_TABLE_MAP).map((v) => v.table));
|
|
14548
14898
|
ALLOWED_COLS = new Set(Object.values(KEY_TABLE_MAP).map((v) => v.col));
|
|
@@ -15827,11 +16177,27 @@ async function setup() {
|
|
|
15827
16177
|
env.GROQ_API_KEY = groqKey;
|
|
15828
16178
|
console.log(green(" Voice transcription (STT) enabled!"));
|
|
15829
16179
|
console.log("");
|
|
15830
|
-
|
|
16180
|
+
console.log(dim(" Choose a voice reply provider:"));
|
|
16181
|
+
console.log(" 1. ElevenLabs (high-quality, requires API key)");
|
|
16182
|
+
console.log(" 2. Grok / xAI (high-quality, requires API key)");
|
|
16183
|
+
console.log(" 3. macOS (free, uses built-in voices \u2014 Samantha, Albert)");
|
|
16184
|
+
console.log(" 4. Skip voice replies");
|
|
16185
|
+
console.log("");
|
|
16186
|
+
const ttsChoice = await requiredInput("Enter choice (1/2/3/4)", "4");
|
|
16187
|
+
if (ttsChoice === "1") {
|
|
15831
16188
|
console.log(dim(" Get an ElevenLabs key at: https://elevenlabs.io/api\n"));
|
|
15832
16189
|
const elevenKey = await requiredInput("ElevenLabs API key", env.ELEVENLABS_API_KEY);
|
|
15833
16190
|
env.ELEVENLABS_API_KEY = elevenKey;
|
|
15834
|
-
console.log(green(" Voice replies
|
|
16191
|
+
console.log(green(" Voice replies via ElevenLabs enabled!"));
|
|
16192
|
+
} else if (ttsChoice === "2") {
|
|
16193
|
+
console.log(dim(" Get an xAI key at: https://console.x.ai/team/default/api-keys\n"));
|
|
16194
|
+
const xaiKey = await requiredInput("xAI API key", env.XAI_API_KEY);
|
|
16195
|
+
env.XAI_API_KEY = xaiKey;
|
|
16196
|
+
console.log(green(" Voice replies via Grok enabled!"));
|
|
16197
|
+
console.log(dim(" Use /voice_config in Telegram to select a voice (Eve, Ara, Rex, Sal, Leo)."));
|
|
16198
|
+
} else if (ttsChoice === "3") {
|
|
16199
|
+
console.log(green(" Voice replies via macOS enabled!"));
|
|
16200
|
+
console.log(dim(" Use /voice_config in Telegram to select a voice (Samantha or Albert)."));
|
|
15835
16201
|
} else {
|
|
15836
16202
|
console.log(dim(" Voice replies will use macOS text-to-speech as fallback."));
|
|
15837
16203
|
}
|
|
@@ -15870,6 +16236,9 @@ async function setup() {
|
|
|
15870
16236
|
if (env.ELEVENLABS_API_KEY) {
|
|
15871
16237
|
envLines.push(`ELEVENLABS_API_KEY=${env.ELEVENLABS_API_KEY}`);
|
|
15872
16238
|
}
|
|
16239
|
+
if (env.XAI_API_KEY) {
|
|
16240
|
+
envLines.push(`XAI_API_KEY=${env.XAI_API_KEY}`);
|
|
16241
|
+
}
|
|
15873
16242
|
}
|
|
15874
16243
|
if (env.DASHBOARD_ENABLED) {
|
|
15875
16244
|
envLines.push("", "# Dashboard", `DASHBOARD_ENABLED=${env.DASHBOARD_ENABLED}`);
|
|
@@ -16161,6 +16530,18 @@ config.command("set <key> <value>").description("Set a runtime config value (via
|
|
|
16161
16530
|
const { configSet: configSet2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
16162
16531
|
await configSet2(program.opts(), key, value);
|
|
16163
16532
|
});
|
|
16533
|
+
config.command("response-style [style]").description("Get or set the AI response style (concise/normal/detailed)").action(async (style) => {
|
|
16534
|
+
const { configGet: configGet2, configSet: configSet2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
16535
|
+
if (style) {
|
|
16536
|
+
if (!["concise", "normal", "detailed"].includes(style)) {
|
|
16537
|
+
console.error("Invalid style. Must be concise, normal, or detailed.");
|
|
16538
|
+
process.exit(1);
|
|
16539
|
+
}
|
|
16540
|
+
await configSet2(program.opts(), "response-style", style);
|
|
16541
|
+
} else {
|
|
16542
|
+
await configGet2(program.opts(), "response-style");
|
|
16543
|
+
}
|
|
16544
|
+
});
|
|
16164
16545
|
config.command("env").description("Print .env path and values (static config, redacted secrets)").action(async () => {
|
|
16165
16546
|
const { configEnv: configEnv2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
16166
16547
|
await configEnv2(program.opts());
|
package/package.json
CHANGED