@phuetz/code-buddy 1.3.2 → 1.4.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 CHANGED
@@ -17,7 +17,7 @@
17
17
  <p align="center">
18
18
  <a href="https://github.com/phuetz/code-buddy/stargazers"><img src="https://img.shields.io/github/stars/phuetz/code-buddy?style=flat-square&logo=github&color=feca57&label=Star" alt="GitHub stars"/></a>
19
19
  <img src="https://img.shields.io/badge/Tests-27K%2B-00d26a?style=flat-square&logo=jest" alt="Tests"/>
20
- <img src="https://img.shields.io/badge/v1.3.1-GA-blueviolet?style=flat-square" alt="Version 1.3.1 GA"/>
20
+ <img src="https://img.shields.io/badge/v1.4.0-GA-blueviolet?style=flat-square" alt="Version 1.4.0 GA"/>
21
21
  </p>
22
22
 
23
23
  <br/>
@@ -101,13 +101,32 @@ An open-source, multi-provider AI coding agent with a terminal UI, an HTTP/WebSo
101
101
  <img src="docs/screenshots/self-audit-bug-1.png" alt="Self-audit bug found" width="820"/>
102
102
  </p>
103
103
 
104
+ **On your phone — chat with the same agent over Telegram.** Code Buddy runs as a messaging-channel bot, so the agent you use in the terminal is reachable from your pocket. Real, unedited captures (the bot is named *"Lisa"* here). The system prompt and tools **scale to each question** — light and instant for plain chat, escalating to load tools only when the request needs them (the same on-demand pattern as Codex / Claude):
105
+
106
+ <table>
107
+ <tr>
108
+ <td width="33%" align="center" valign="top">
109
+ <img src="docs/screenshots/telegram-companion-chat.jpg" alt="Telegram chat: instant greeting, the time, and tomorrow's live weather in Paris via web search" width="250"/><br/>
110
+ <sub><b>Chat + live tools, on demand</b><br/>"Bonjour" answers instantly; <i>"what time is it?"</i> and <i>"tomorrow's weather in Paris?"</i> pull the time and <b><code>web_search</code></b> tools — only when actually asked.</sub>
111
+ </td>
112
+ <td width="33%" align="center" valign="top">
113
+ <img src="docs/screenshots/telegram-companion-selfcode.jpg" alt="Telegram chat: the agent confirms it can read and inspect its own source code via view_file" width="250"/><br/>
114
+ <sub><b>Reads its own code</b><br/>Confirms it can inspect its own source (or any accessible file) via <code>view_file</code> — then introduces its recursive self-improvement →</sub>
115
+ </td>
116
+ <td width="33%" align="center" valign="top">
117
+ <img src="docs/screenshots/telegram-companion-recursive.jpg" alt="Telegram chat: the agent explains its recursive self-improvement — Manus-inspired lessons in RULE / PATTERN / CONTEXT categories, stored in .codebuddy/lessons.md" width="250"/><br/>
118
+ <sub><b>Improves itself across sessions</b><br/>The <code>lessons_*</code> loop (Manus-inspired): after each fix or success it extracts <b>RULE / PATTERN / CONTEXT</b> lessons, persisted to <code>.codebuddy/lessons.md</code> (project + global). <i>Accurate — matches its real source.</i></sub>
119
+ </td>
120
+ </tr>
121
+ </table>
122
+
104
123
  More desktop demos (Fleet, Autonomy, Companion, …) and captures: [`cowork/readme.md`](cowork/readme.md#demo) · [`docs/screenshots/`](docs/screenshots/README.md).
105
124
 
106
125
  ---
107
126
 
108
127
  ## What's shipped
109
128
 
110
- **1.2.0 GA — these aren't roadmap items.** The captures above are unedited, and the core runs today:
129
+ **1.4.0 GA — these aren't roadmap items.** The captures above are unedited, and the core runs today:
111
130
 
112
131
  - ✅ **`$0` local coding agent** — a local Ollama model reasons on screen, then calls tools to do real work. *(the demos above)*
113
132
  - ✅ **ChatGPT Plus/Pro → `gpt-5.5` at `$0`** — `buddy login`, flat-fee, no API key, no per-token metering.
@@ -39,7 +39,10 @@ const DEFAULT_CONFIG = {
39
39
  // "agent proposes, human approves" path, so the model must always see it (a
40
40
  // RAG-gated propose tool would rarely surface). `lessons_list` stays out
41
41
  // (admin-style, not needed per-turn). Wakes the dormant feature in lessons-tracker.ts.
42
- alwaysInclude: ['view_file', 'bash', 'search', 'str_replace_editor', 'web_search', 'remember', 'memory_propose', 'lessons_add', 'lessons_propose', 'lessons_search'],
42
+ // `tool_search` is ALWAYS exposed so the model can discover & pull ANY tool
43
+ // on demand when the per-query TF-IDF subset missed it (progressive disclosure,
44
+ // like Codex/Claude). Without it, a tool outside the top-K is unreachable.
45
+ alwaysInclude: ['view_file', 'bash', 'search', 'str_replace_editor', 'web_search', 'remember', 'memory_propose', 'lessons_add', 'lessons_propose', 'lessons_search', 'tool_search'],
43
46
  useAdaptiveThreshold: true,
44
47
  enableCaching: true,
45
48
  cacheTTLMs: 5 * 60 * 1000, // 5 minutes
@@ -399,6 +399,21 @@ export async function getAllCodeBuddyTools() {
399
399
  ? { ...t, function: { ...t.function, description: (t.function.description ?? '') + DEFER } }
400
400
  : t);
401
401
  }
402
+ // Populate the `tool_search` BM25 index so the model can DISCOVER any tool on
403
+ // demand (progressive disclosure, like Codex/Claude). It was never
404
+ // initialized, leaving tool_search blind ("No tools found"). Index the full
405
+ // set (built-in + MCP + plugin) so a tool outside the per-query TF-IDF subset
406
+ // is still reachable when the model calls tool_search.
407
+ try {
408
+ const { initToolSearchIndex } = await import('../tools/tool-search.js');
409
+ initToolSearchIndex(allTools.map((t) => ({
410
+ name: t.function.name,
411
+ description: t.function.description ?? '',
412
+ })));
413
+ }
414
+ catch {
415
+ // tool-search is optional — never block tool assembly.
416
+ }
402
417
  return allTools;
403
418
  }
404
419
  // ============================================================================
@@ -327,13 +327,8 @@ export async function registerAIMessageHandler(manager) {
327
327
  aiHandlerRegistered = true;
328
328
  manager.onMessage(async (message, channel) => {
329
329
  try {
330
- const apiKey = process.env.GROK_API_KEY || process.env.XAI_API_KEY || '';
331
- if (!apiKey) {
332
- logger.warn('No API key for channel AI responses');
333
- return;
334
- }
335
- const { checkDMPairing, getDMPairing, getRouteAgentConfig } = await import('../../channels/core.js');
336
- // 1. Check DM pairing first
330
+ // 1. DM pairing gate unapproved senders get a code, then we stop.
331
+ const { checkDMPairing, getDMPairing } = await import('../../channels/core.js');
337
332
  const pairingStatus = await checkDMPairing(message);
338
333
  if (!pairingStatus.approved) {
339
334
  if (pairingStatus.code) {
@@ -347,14 +342,35 @@ export async function registerAIMessageHandler(manager) {
347
342
  }
348
343
  return;
349
344
  }
350
- // 2. Resolve route-backed agent config
351
- const agentConfig = getRouteAgentConfig(message);
352
- // 3. Instantiate Agent with routed config
345
+ // Nothing to answer (e.g. a non-text message with no transcription).
346
+ if (!message.content || !message.content.trim()) {
347
+ return;
348
+ }
349
+ // 2. Context-adaptive agent reply (« comme Claude »): the agent's own
350
+ // query-classifier + buildForQuery scale the system prompt to the
351
+ // request (a greeting → minimal ~800B prompt, NOT the 73KB legacy),
352
+ // and tools load on demand — RAG selects only the relevant ~15 and the
353
+ // `tool_search` meta-tool pulls more when actually needed. Bounded
354
+ // rounds keep a simple chat fast while a real task can still act.
355
+ const { resolveProviderFromEnv } = await import('../../fleet/peer-chat-client-factory.js');
356
+ const knownProviders = ['ollama', 'chatgpt', 'gemini', 'grok', 'anthropic'];
357
+ const preferredProvider = process.env.CODEBUDDY_PROVIDER && knownProviders.includes(process.env.CODEBUDDY_PROVIDER)
358
+ ? process.env.CODEBUDDY_PROVIDER
359
+ : 'auto';
360
+ const resolved = resolveProviderFromEnv(preferredProvider);
361
+ if (!resolved) {
362
+ logger.warn('No LLM provider for channel chat — set CODEBUDDY_PROVIDER + a provider key/env');
363
+ return;
364
+ }
365
+ const { getRouteAgentConfig } = await import('../../channels/core.js');
353
366
  const { CodeBuddyAgent } = await import('../../agent/codebuddy-agent.js');
354
- const model = agentConfig.model || process.env.GROK_MODEL || 'grok-3-latest';
355
- const maxRounds = agentConfig.maxToolRounds;
356
- const agent = new CodeBuddyAgent(apiKey, process.env.GROK_BASE_URL, model, maxRounds);
357
- // 4. Resume/Initialize session history
367
+ const agentConfig = getRouteAgentConfig(message);
368
+ const model = agentConfig.model || resolved.model;
369
+ const agent = new CodeBuddyAgent(resolved.apiKey || 'local', resolved.baseUrl, model, agentConfig.maxToolRounds ?? 6, // bounded (vs the 50-round default)
370
+ true, // useRAGToolSelection relevant tools on demand, not all ~194
371
+ process.env.CODEBUDDY_CHANNEL_PROMPT_ID || 'auto', // minimal/adaptive prompt, not the 73KB legacy
372
+ process.cwd());
373
+ // Multi-turn: restore prior session history into the agent.
358
374
  const sessionKey = message.sessionKey || 'default-global';
359
375
  const sessionStore = agent.getSessionStore();
360
376
  let session = await sessionStore.loadSession(sessionKey);
@@ -371,18 +387,16 @@ export async function registerAIMessageHandler(manager) {
371
387
  await sessionStore.saveSession(session);
372
388
  }
373
389
  await sessionStore.resumeSession(sessionKey);
374
- const activeSession = session;
375
- if (activeSession.messages && activeSession.messages.length > 0) {
376
- const chatHistory = sessionStore.convertMessagesToChatEntries(activeSession.messages);
377
- const messages = activeSession.messages.map(m => ({
390
+ if (session.messages && session.messages.length > 0) {
391
+ const chatHistory = sessionStore.convertMessagesToChatEntries(session.messages);
392
+ const priorMessages = session.messages.map((m) => ({
378
393
  role: m.type === 'user' ? 'user' : 'assistant',
379
- content: m.content
394
+ content: m.content,
380
395
  }));
381
396
  const historyRestorer = agent;
382
397
  historyRestorer.historyManager.setChatHistory(chatHistory);
383
- historyRestorer.historyManager.setMessages(messages);
398
+ historyRestorer.historyManager.setMessages(priorMessages);
384
399
  }
385
- // 5. Run agent turn
386
400
  const entries = await agent.processUserMessage(message.content);
387
401
  const lastEntry = entries[entries.length - 1];
388
402
  const response = lastEntry ? String(lastEntry.content) : '';
@@ -412,7 +426,10 @@ export async function instantiateChannel(config) {
412
426
  switch (config.type) {
413
427
  case 'telegram': {
414
428
  const { TelegramChannel } = await import('../../channels/telegram/index.js');
415
- return new TelegramChannel({ botToken: config.token || '', ...opts });
429
+ // TelegramChannel reads `config.token` (client.ts) pass `token`, not
430
+ // `botToken`, or it throws "Telegram bot token is required" and the
431
+ // channel never starts from channels.json / server intake.
432
+ return new TelegramChannel({ token: config.token || '', ...opts });
416
433
  }
417
434
  case 'discord': {
418
435
  const { DiscordChannel } = await import('../../channels/discord/index.js');
@@ -272,8 +272,23 @@ export class TextToSpeechManager extends EventEmitter {
272
272
  async speakWithPiper(text) {
273
273
  return new Promise((resolve, reject) => {
274
274
  const audioFile = path.join(this.tempDir, `tts_${Date.now()}.wav`);
275
+ // Resolve the Piper binary + voice model. Defaults to `piper` on PATH
276
+ // (no model), but honors the local-voice-stack convention shared with
277
+ // Cowork so a self-hosted Piper (e.g. ai-stack fr_FR-siwis-medium) works
278
+ // out of the box: COWORK_PIPER_BIN / COWORK_PIPER_VOICE (or the
279
+ // CODEBUDDY_* aliases, or a `.onnx` path set as the configured voice).
280
+ const piperBin = process.env.COWORK_PIPER_BIN || process.env.CODEBUDDY_PIPER_BIN || 'piper';
281
+ const voiceModel = process.env.COWORK_PIPER_VOICE ||
282
+ process.env.CODEBUDDY_PIPER_VOICE ||
283
+ (this.config.voice && this.config.voice.endsWith('.onnx')
284
+ ? this.config.voice
285
+ : undefined);
286
+ const piperArgs = ['--output_file', audioFile];
287
+ if (voiceModel) {
288
+ piperArgs.push('--model', voiceModel);
289
+ }
275
290
  // Piper reads from stdin and outputs to file
276
- const piper = spawn('piper', ['--output_file', audioFile]);
291
+ const piper = spawn(piperBin, piperArgs);
277
292
  piper.stdin?.write(text);
278
293
  piper.stdin?.end();
279
294
  piper.on('close', (code) => {
@@ -0,0 +1,9 @@
1
+ export interface LocalWhisperOptions {
2
+ language?: string;
3
+ model?: string;
4
+ timeoutMs?: number;
5
+ }
6
+ /** Transcribe an audio file (wav/ogg/mp3/webm…) to text via local faster-whisper. */
7
+ export declare function transcribeFile(audioPath: string, options?: LocalWhisperOptions): Promise<string>;
8
+ /** True if a local faster-whisper interpreter looks available. */
9
+ export declare function localWhisperAvailable(): boolean;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Local speech-to-text via faster-whisper ($0, offline).
3
+ *
4
+ * Wraps a self-hosted faster-whisper venv (the same local voice stack Cowork
5
+ * uses — e.g. ~/DEV/ai-stack/voice/.venv) as a reusable `audioFile -> text`
6
+ * helper. One-shot per call (model loads in ~1-2s on int8 CPU), which is fine
7
+ * for occasional clips (Telegram voice notes, a CLI turn). No cloud, no key.
8
+ *
9
+ * Engine selection (first existing wins):
10
+ * CODEBUDDY_VOICE_PYTHON | COWORK_VOICE_PYTHON (explicit interpreter)
11
+ * ~/.codebuddy/voice/.venv/bin/python
12
+ * ~/DEV/ai-stack/voice/.venv/bin/python
13
+ * ~/ai-stack/voice/.venv/bin/python
14
+ * python3 (PATH fallback)
15
+ * Model: CODEBUDDY_WHISPER_MODEL | COWORK_WHISPER_MODEL | "base".
16
+ */
17
+ import { spawn } from 'node:child_process';
18
+ import { existsSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { join } from 'node:path';
21
+ function resolvePython() {
22
+ const candidates = [
23
+ process.env.CODEBUDDY_VOICE_PYTHON,
24
+ process.env.COWORK_VOICE_PYTHON,
25
+ join(homedir(), '.codebuddy/voice/.venv/bin/python'),
26
+ join(homedir(), 'DEV/ai-stack/voice/.venv/bin/python'),
27
+ join(homedir(), 'ai-stack/voice/.venv/bin/python'),
28
+ ].filter((c) => Boolean(c));
29
+ for (const c of candidates) {
30
+ if (existsSync(c))
31
+ return c;
32
+ }
33
+ return 'python3';
34
+ }
35
+ const WHISPER_SCRIPT = `
36
+ import sys, json
37
+ from faster_whisper import WhisperModel
38
+ audio, model, lang = sys.argv[1], sys.argv[2], (sys.argv[3] or None)
39
+ m = WhisperModel(model, device="cpu", compute_type="int8")
40
+ segs, _info = m.transcribe(audio, language=lang, vad_filter=True, beam_size=1)
41
+ print(json.dumps({"text": " ".join(s.text.strip() for s in segs).strip()}))
42
+ `;
43
+ /** Transcribe an audio file (wav/ogg/mp3/webm…) to text via local faster-whisper. */
44
+ export async function transcribeFile(audioPath, options = {}) {
45
+ const language = options.language ?? 'fr';
46
+ const model = options.model ||
47
+ process.env.CODEBUDDY_WHISPER_MODEL ||
48
+ process.env.COWORK_WHISPER_MODEL ||
49
+ 'base';
50
+ const timeoutMs = options.timeoutMs ?? 120_000;
51
+ if (!existsSync(audioPath)) {
52
+ throw new Error(`local-whisper: audio file not found: ${audioPath}`);
53
+ }
54
+ return new Promise((resolve, reject) => {
55
+ const py = resolvePython();
56
+ const proc = spawn(py, ['-c', WHISPER_SCRIPT, audioPath, model, language], {
57
+ stdio: ['ignore', 'pipe', 'pipe'],
58
+ });
59
+ let stdout = '';
60
+ let stderr = '';
61
+ const timer = setTimeout(() => {
62
+ proc.kill('SIGKILL');
63
+ reject(new Error(`local-whisper: timed out after ${timeoutMs}ms`));
64
+ }, timeoutMs);
65
+ proc.stdout.on('data', (d) => (stdout += d.toString()));
66
+ proc.stderr.on('data', (d) => (stderr += d.toString()));
67
+ proc.on('error', (e) => {
68
+ clearTimeout(timer);
69
+ reject(new Error(`local-whisper: failed to spawn ${py}: ${e.message}`));
70
+ });
71
+ proc.on('close', (code) => {
72
+ clearTimeout(timer);
73
+ if (code !== 0) {
74
+ reject(new Error(`local-whisper: exited ${code}: ${stderr.trim().slice(-300)}`));
75
+ return;
76
+ }
77
+ try {
78
+ const line = stdout.trim().split('\n').filter(Boolean).pop() || '{}';
79
+ const parsed = JSON.parse(line);
80
+ resolve((parsed.text || '').trim());
81
+ }
82
+ catch (e) {
83
+ reject(new Error(`local-whisper: bad output: ${String(e)} :: ${stdout.slice(-200)}`));
84
+ }
85
+ });
86
+ });
87
+ }
88
+ /** True if a local faster-whisper interpreter looks available. */
89
+ export function localWhisperAvailable() {
90
+ const py = resolvePython();
91
+ return py !== 'python3' || Boolean(process.env.CODEBUDDY_VOICE_PYTHON);
92
+ }
93
+ //# sourceMappingURL=local-whisper.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phuetz/code-buddy",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "Open-source multi-provider AI coding agent for the terminal, desktop, and HTTP. 15 LLM providers (Grok, Claude, ChatGPT, Gemini, Ollama, LM Studio, …) with ~110 tools, a peer-to-peer fleet, opt-in self-improvement, multi-channel messaging, and a skills system.",
5
5
  "author": "Patrice Huetz <patrice.huetz@gmail.com>",
6
6
  "repository": {