alvin-bot 4.18.5 โ 4.19.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/CHANGELOG.md +42 -0
- package/dist/handlers/message.js +13 -4
- package/dist/handlers/platform-message.js +8 -3
- package/dist/providers/claude-sdk-provider.js +3 -0
- package/dist/providers/registry.js +12 -2
- package/dist/services/voice.js +16 -4
- package/dist/services/workspaces.js +27 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.19.0] โ 2026-04-24
|
|
6
|
+
|
|
7
|
+
### ๐งญ Feature: per-workspace runtime overrides (effort ยท provider ยท voice ยท temperature ยท toolset)
|
|
8
|
+
|
|
9
|
+
Workspaces could already override `model` and `cwd`. v4.19.0 extends the YAML frontmatter with five more runtime fields that take effect automatically the moment a user runs `/workspace <name>`.
|
|
10
|
+
|
|
11
|
+
**New workspace frontmatter fields** (`~/.alvin-bot/workspaces/<name>.md`):
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
---
|
|
15
|
+
purpose: my-project
|
|
16
|
+
cwd: ~/path/to/workdir
|
|
17
|
+
model: opus # already existed
|
|
18
|
+
effort: high # NEW โ low | medium | high
|
|
19
|
+
provider: claude-sdk # NEW โ registry key; fallback chain still applies
|
|
20
|
+
voice: iP95p4xoKVk53GoZ742B # NEW โ ElevenLabs voice ID, or Edge-TTS voice name (e.g. "en-US-JennyNeural")
|
|
21
|
+
temperature: 0.3 # NEW โ 0โ2 sampling temperature
|
|
22
|
+
toolset: research # NEW โ full (default) | readonly | research
|
|
23
|
+
---
|
|
24
|
+
Persona body continues here...
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Toolset presets** (map to concrete `allowedTools` lists via the new exported `toolsetToAllowedTools` helper):
|
|
28
|
+
- `full` โ provider default (`Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, Task` + MCP)
|
|
29
|
+
- `readonly` โ `Read, Glob, Grep, WebSearch, WebFetch` (no Write/Edit/Bash)
|
|
30
|
+
- `research` โ `Read, WebSearch, WebFetch, Grep` (pure research mode)
|
|
31
|
+
|
|
32
|
+
**Implementation:**
|
|
33
|
+
|
|
34
|
+
- `src/services/workspaces.ts` โ `Workspace` interface + `parseFrontmatter` extended; numeric parsing + enum validation so a malformed value is silently dropped and the session-default wins.
|
|
35
|
+
- `src/handlers/message.ts` + `src/handlers/platform-message.ts` โ at query-assembly time, every workspace-set field overrides the equivalent session/registry default exactly for this one query. Nothing sticky leaks across workspace switches.
|
|
36
|
+
- `src/providers/registry.ts` โ `queryWithFallback()` gains optional `providerOverride`. When supplied AND registered, it becomes primary for that query; fallback chain still applies, and the globally active provider joins the chain as a last-resort backup so availability drops still degrade gracefully.
|
|
37
|
+
- `src/providers/claude-sdk-provider.ts` โ passes `options.temperature` through to the Agent SDK when set.
|
|
38
|
+
- `src/services/voice.ts` โ `textToSpeech(text, voice?)` โ optional second arg; picked up from `workspace.voice` in Telegram handler's voice-reply path. Works for both ElevenLabs (Voice ID) and Edge TTS (Voice Name like `de-DE-ConradNeural`).
|
|
39
|
+
|
|
40
|
+
**Net effect:** Each workspace becomes a self-contained runtime profile. For example:
|
|
41
|
+
- A `prep` workspace with `model: opus ยท effort: high ยท temperature: 0.3 ยท voice: en-US-JennyNeural` for polished long-form work;
|
|
42
|
+
- A `research` workspace with `toolset: research ยท model: haiku ยท effort: low ยท temperature: 0.7` for cheap-and-fast web spelunking;
|
|
43
|
+
- A `sensitive` workspace with `toolset: readonly ยท provider: claude-sdk` so the agent cannot accidentally `Write` or `Bash` inside the cwd.
|
|
44
|
+
|
|
45
|
+
No data migration required โ existing workspace files without the new fields keep working identically.
|
|
46
|
+
|
|
5
47
|
## [4.18.5] โ 2026-04-23
|
|
6
48
|
|
|
7
49
|
### ๐ Fix: auto-reset stale SDK sessionId on empty-stream detection
|
package/dist/handlers/message.js
CHANGED
|
@@ -381,14 +381,23 @@ export async function handleMessage(ctx) {
|
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
+
// v4.19.0 โ Per-workspace runtime overrides. Each is only applied when
|
|
385
|
+
// the workspace explicitly set it; otherwise the session/provider default
|
|
386
|
+
// wins. Toolset is mapped to a concrete allowedTools list via
|
|
387
|
+
// toolsetToAllowedTools(); providers that ignore allowedTools (Ollama etc.)
|
|
388
|
+
// just drop it.
|
|
389
|
+
const { toolsetToAllowedTools } = await import("../services/workspaces.js");
|
|
390
|
+
const wsAllowed = toolsetToAllowedTools(workspace.toolset);
|
|
384
391
|
const queryOpts = {
|
|
385
392
|
prompt: bridgedPrompt,
|
|
386
393
|
systemPrompt,
|
|
387
394
|
workingDir: session.workingDir,
|
|
388
|
-
effort: session.effort,
|
|
395
|
+
effort: workspace.effort ?? session.effort,
|
|
389
396
|
// v4.15 โ Per-workspace model override (optional YAML `model:` field).
|
|
390
|
-
//
|
|
397
|
+
// v4.19 โ ditto for temperature and toolset-derived allowedTools.
|
|
391
398
|
...(workspace.model ? { model: workspace.model } : {}),
|
|
399
|
+
...(workspace.temperature !== undefined ? { temperature: workspace.temperature } : {}),
|
|
400
|
+
...(wsAllowed ? { allowedTools: wsAllowed } : {}),
|
|
392
401
|
abortSignal: session.abortController.signal,
|
|
393
402
|
// User's UI locale โ registry uses it to localize failure messages.
|
|
394
403
|
locale: session.language,
|
|
@@ -420,7 +429,7 @@ export async function handleMessage(ctx) {
|
|
|
420
429
|
// readable description (which only appears in the tool_use input,
|
|
421
430
|
// not in the tool_result text). See Fix #17 Stage 2.
|
|
422
431
|
let lastAgentToolUseInput;
|
|
423
|
-
for await (const chunk of registry.queryWithFallback(queryOpts)) {
|
|
432
|
+
for await (const chunk of registry.queryWithFallback(queryOpts, workspace.provider)) {
|
|
424
433
|
// v4.12.1 โ Update pending-sync-task state FIRST so the timer's
|
|
425
434
|
// next reset picks up the new state. This ordering is load-bearing:
|
|
426
435
|
// reversing it means the timer rearms with stale state. A sync
|
|
@@ -608,7 +617,7 @@ export async function handleMessage(ctx) {
|
|
|
608
617
|
if (session.voiceReply && finalText.trim()) {
|
|
609
618
|
try {
|
|
610
619
|
await ctx.api.sendChatAction(ctx.chat.id, "upload_voice");
|
|
611
|
-
const audioPath = await textToSpeech(finalText);
|
|
620
|
+
const audioPath = await textToSpeech(finalText, workspace.voice);
|
|
612
621
|
await ctx.replyWithVoice(new InputFile(fs.readFileSync(audioPath), "response.mp3"));
|
|
613
622
|
fs.unlink(audioPath, () => { });
|
|
614
623
|
}
|
|
@@ -164,14 +164,19 @@ export async function handlePlatformMessage(msg, adapter) {
|
|
|
164
164
|
// in the system prompt for this query.
|
|
165
165
|
const isFirstSDKTurn = isSDK && session.sessionId === null;
|
|
166
166
|
const systemPrompt = (await buildSmartSystemPrompt(isSDK, session.language, fullText, msg.chatId, isFirstSDKTurn, workspace.systemPromptOverride)) + skillContext;
|
|
167
|
+
// v4.19.0 โ Per-workspace runtime overrides (model/effort/temperature/toolset).
|
|
168
|
+
const { toolsetToAllowedTools } = await import("../services/workspaces.js");
|
|
169
|
+
const wsAllowed = toolsetToAllowedTools(workspace.toolset);
|
|
167
170
|
const queryOpts = {
|
|
168
171
|
prompt: fullText,
|
|
169
172
|
systemPrompt,
|
|
170
173
|
workingDir: session.workingDir,
|
|
171
|
-
effort: session.effort,
|
|
174
|
+
effort: workspace.effort ?? session.effort,
|
|
172
175
|
// v4.15 โ Per-workspace model override (optional YAML `model:` field).
|
|
173
|
-
//
|
|
176
|
+
// v4.19 โ ditto for temperature and toolset-derived allowedTools.
|
|
174
177
|
...(workspace.model ? { model: workspace.model } : {}),
|
|
178
|
+
...(workspace.temperature !== undefined ? { temperature: workspace.temperature } : {}),
|
|
179
|
+
...(wsAllowed ? { allowedTools: wsAllowed } : {}),
|
|
175
180
|
sessionId: isSDK ? session.sessionId : null,
|
|
176
181
|
history: !isSDK ? session.history : undefined,
|
|
177
182
|
// v4.14 โ Expose alvin_dispatch_agent MCP tool on non-Telegram
|
|
@@ -190,7 +195,7 @@ export async function handlePlatformMessage(msg, adapter) {
|
|
|
190
195
|
if (!isSDK) {
|
|
191
196
|
addToHistory(sessionKey, { role: "user", content: fullText });
|
|
192
197
|
}
|
|
193
|
-
for await (const chunk of registry.queryWithFallback(queryOpts)) {
|
|
198
|
+
for await (const chunk of registry.queryWithFallback(queryOpts, workspace.provider)) {
|
|
194
199
|
switch (chunk.type) {
|
|
195
200
|
case "text":
|
|
196
201
|
finalText = chunk.text || "";
|
|
@@ -192,6 +192,9 @@ export class ClaudeSDKProvider {
|
|
|
192
192
|
maxTurns: 50,
|
|
193
193
|
betas: ["context-1m-2025-08-07"],
|
|
194
194
|
...(modelOverride ? { model: modelOverride } : {}),
|
|
195
|
+
// v4.19.0 โ per-workspace temperature override. Passed through to
|
|
196
|
+
// the Agent SDK; providers that don't use it just drop it.
|
|
197
|
+
...(typeof options.temperature === "number" ? { temperature: options.temperature } : {}),
|
|
195
198
|
// Prefer Haiku as fallback on rate-limit/overload โ cheap and
|
|
196
199
|
// fast, keeps the bot responsive when the primary tier is
|
|
197
200
|
// throttled. Omitted when the primary IS Haiku (SDK requires
|
|
@@ -109,8 +109,18 @@ export class ProviderRegistry {
|
|
|
109
109
|
* and asking the user to retry. The failover is only silent when
|
|
110
110
|
* the failing provider hadn't committed any visible text yet.
|
|
111
111
|
*/
|
|
112
|
-
async *queryWithFallback(options) {
|
|
113
|
-
|
|
112
|
+
async *queryWithFallback(options, providerOverride) {
|
|
113
|
+
// v4.19.0 โ Per-workspace provider override. If supplied AND registered,
|
|
114
|
+
// it becomes the primary for this query; fallbacks still apply so the
|
|
115
|
+
// bot can degrade gracefully if the override provider is unavailable.
|
|
116
|
+
const primary = providerOverride && this.providers.has(providerOverride)
|
|
117
|
+
? providerOverride
|
|
118
|
+
: this.activeKey;
|
|
119
|
+
const chain = [primary, ...this.fallbackKeys.filter(k => k !== primary)];
|
|
120
|
+
// Also include activeKey as a last-resort fallback if override was used
|
|
121
|
+
if (providerOverride && !chain.includes(this.activeKey)) {
|
|
122
|
+
chain.push(this.activeKey);
|
|
123
|
+
}
|
|
114
124
|
const errors = [];
|
|
115
125
|
for (const key of chain) {
|
|
116
126
|
const provider = this.providers.get(key);
|
package/dist/services/voice.js
CHANGED
|
@@ -55,7 +55,16 @@ export async function transcribeAudio(audioPath) {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
// โโ Text-to-Speech (Edge TTS via node-edge-tts) โโโโโโโโ
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Convert text to speech and return path to the MP3 file.
|
|
60
|
+
*
|
|
61
|
+
* @param text The text to synthesize.
|
|
62
|
+
* @param voice Optional voice ID / name override (v4.19.0 โ per-workspace).
|
|
63
|
+
* For ElevenLabs this is the Voice ID, for Edge TTS the Voice Name
|
|
64
|
+
* (e.g. "de-DE-ConradNeural", "en-US-JennyNeural"). When undefined,
|
|
65
|
+
* the config default is used.
|
|
66
|
+
*/
|
|
67
|
+
export async function textToSpeech(text, voice) {
|
|
59
68
|
// Strip markdown formatting for cleaner TTS
|
|
60
69
|
let cleanText = text
|
|
61
70
|
.replace(/```[\s\S]*?```/g, " Code block skipped. ")
|
|
@@ -77,7 +86,9 @@ export async function textToSpeech(text) {
|
|
|
77
86
|
if (config.ttsProvider === "elevenlabs" && config.elevenlabs.apiKey) {
|
|
78
87
|
try {
|
|
79
88
|
const { elevenLabsTTS } = await import("./elevenlabs.js");
|
|
80
|
-
|
|
89
|
+
// v4.19.0 โ per-workspace voice override. When unset, elevenLabsTTS falls
|
|
90
|
+
// back to config.elevenlabs.voiceId.
|
|
91
|
+
return await elevenLabsTTS(cleanText, voice);
|
|
81
92
|
}
|
|
82
93
|
catch (err) {
|
|
83
94
|
console.warn("ElevenLabs TTS failed, falling back to Edge TTS:", err instanceof Error ? err.message : err);
|
|
@@ -86,8 +97,9 @@ export async function textToSpeech(text) {
|
|
|
86
97
|
// Edge TTS (default / fallback)
|
|
87
98
|
const outputPath = path.join(TEMP_DIR, `tts_${Date.now()}.mp3`);
|
|
88
99
|
const tts = new EdgeTTS({
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
// v4.19.0 โ allow workspace override; default German male.
|
|
101
|
+
voice: voice || "de-DE-ConradNeural",
|
|
102
|
+
lang: voice && voice.match(/^[a-z]{2}-[A-Z]{2}/) ? voice.slice(0, 5) : "de-DE",
|
|
91
103
|
outputFormat: "audio-24khz-48kbitrate-mono-mp3",
|
|
92
104
|
});
|
|
93
105
|
await tts.ttsPromise(cleanText, outputPath);
|
|
@@ -32,6 +32,16 @@ import os from "os";
|
|
|
32
32
|
import path from "path";
|
|
33
33
|
import { WORKSPACES_DIR } from "../paths.js";
|
|
34
34
|
import { config } from "../config.js";
|
|
35
|
+
/** Map a toolset preset to the concrete allowedTools list. */
|
|
36
|
+
export function toolsetToAllowedTools(toolset) {
|
|
37
|
+
if (!toolset || toolset === "full")
|
|
38
|
+
return undefined; // undefined = use provider default
|
|
39
|
+
if (toolset === "readonly")
|
|
40
|
+
return ["Read", "Glob", "Grep", "WebSearch", "WebFetch"];
|
|
41
|
+
if (toolset === "research")
|
|
42
|
+
return ["Read", "WebSearch", "WebFetch", "Grep"];
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
35
45
|
const registry = new Map();
|
|
36
46
|
/** Expand ~ at the start of a path to the user's home directory. */
|
|
37
47
|
function expandHome(p) {
|
|
@@ -100,6 +110,18 @@ function readWorkspaceFile(filePath, name) {
|
|
|
100
110
|
const color = typeof fm.color === "string" ? fm.color : undefined;
|
|
101
111
|
const emoji = typeof fm.emoji === "string" ? fm.emoji : undefined;
|
|
102
112
|
const model = typeof fm.model === "string" && fm.model.trim() ? fm.model.trim() : undefined;
|
|
113
|
+
// v4.19.0 โ per-workspace runtime overrides
|
|
114
|
+
const effortRaw = typeof fm.effort === "string" ? fm.effort.trim().toLowerCase() : "";
|
|
115
|
+
const effort = (effortRaw === "low" || effortRaw === "medium" || effortRaw === "high")
|
|
116
|
+
? effortRaw : undefined;
|
|
117
|
+
const provider = typeof fm.provider === "string" && fm.provider.trim() ? fm.provider.trim() : undefined;
|
|
118
|
+
const voice = typeof fm.voice === "string" && fm.voice.trim() ? fm.voice.trim() : undefined;
|
|
119
|
+
const temperatureRaw = typeof fm.temperature === "string" ? parseFloat(fm.temperature) : (typeof fm.temperature === "number" ? fm.temperature : NaN);
|
|
120
|
+
const temperature = Number.isFinite(temperatureRaw) && temperatureRaw >= 0 && temperatureRaw <= 2
|
|
121
|
+
? temperatureRaw : undefined;
|
|
122
|
+
const toolsetRaw = typeof fm.toolset === "string" ? fm.toolset.trim().toLowerCase() : "";
|
|
123
|
+
const toolset = (toolsetRaw === "full" || toolsetRaw === "readonly" || toolsetRaw === "research")
|
|
124
|
+
? toolsetRaw : undefined;
|
|
103
125
|
const channels = Array.isArray(fm.channels)
|
|
104
126
|
? fm.channels.filter((c) => typeof c === "string")
|
|
105
127
|
: [];
|
|
@@ -111,6 +133,11 @@ function readWorkspaceFile(filePath, name) {
|
|
|
111
133
|
emoji,
|
|
112
134
|
channels,
|
|
113
135
|
model,
|
|
136
|
+
effort,
|
|
137
|
+
provider,
|
|
138
|
+
voice,
|
|
139
|
+
temperature,
|
|
140
|
+
toolset,
|
|
114
141
|
systemPromptOverride: body.trim(),
|
|
115
142
|
};
|
|
116
143
|
}
|