codeclaw 0.2.6 → 0.2.8

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.
@@ -12,6 +12,24 @@ import { Bot, VERSION, fmtTokens, fmtUptime, fmtBytes, whichSync, listSubdirs, b
12
12
  import { TelegramChannel } from './channel-telegram.js';
13
13
  import { splitText } from './channel-base.js';
14
14
  // ---------------------------------------------------------------------------
15
+ // Context window sizes (max input tokens per model family)
16
+ // ---------------------------------------------------------------------------
17
+ function getContextWindowSize(model) {
18
+ if (!model)
19
+ return null;
20
+ const m = model.toLowerCase();
21
+ // Claude models: 200k context
22
+ if (m.includes('claude'))
23
+ return 200_000;
24
+ // GPT-4.1 / GPT-5 / o3/o4 family: 200k (1M for o3, but default to 200k)
25
+ if (m.startsWith('gpt-') || m.startsWith('o3') || m.startsWith('o4'))
26
+ return 200_000;
27
+ // Gemini: 1M context
28
+ if (m.includes('gemini'))
29
+ return 1_000_000;
30
+ return null;
31
+ }
32
+ // ---------------------------------------------------------------------------
15
33
  // Telegram HTML formatting
16
34
  // ---------------------------------------------------------------------------
17
35
  function escapeHtml(t) {
@@ -321,6 +339,7 @@ export class TelegramBot extends Bot {
321
339
  static MENU_COMMANDS = [
322
340
  { command: 'sessions', description: 'List / switch sessions' },
323
341
  { command: 'agents', description: 'List / switch agents' },
342
+ { command: 'models', description: 'List / switch models' },
324
343
  { command: 'status', description: 'Bot status' },
325
344
  { command: 'host', description: 'Host machine info' },
326
345
  { command: 'switch', description: 'Switch working directory' },
@@ -336,6 +355,7 @@ export class TelegramBot extends Bot {
336
355
  await ctx.reply(`<b>codeclaw</b> v${VERSION}\n\n` +
337
356
  `/sessions \u2014 List / switch sessions\n` +
338
357
  `/agents \u2014 List / switch agents\n` +
358
+ `/models \u2014 List / switch models\n` +
339
359
  `/status \u2014 Bot status\n` +
340
360
  `/host \u2014 Host machine info\n` +
341
361
  `/switch \u2014 Switch working directory\n` +
@@ -454,6 +474,22 @@ export class TelegramBot extends Bot {
454
474
  }
455
475
  await ctx.reply(lines.join('\n'), { parseMode: 'HTML', keyboard: { inline_keyboard: rows } });
456
476
  }
477
+ async cmdModels(ctx) {
478
+ const cs = this.chat(ctx.chatId);
479
+ const res = this.fetchModels(cs.agent);
480
+ const currentModel = this.modelForAgent(cs.agent);
481
+ const lines = [`<b>Models for ${escapeHtml(cs.agent)}</b>\n`];
482
+ const rows = [];
483
+ for (const m of res.models) {
484
+ const isCurrent = m.id === currentModel || m.alias === currentModel;
485
+ const status = isCurrent ? '\u25CF' : '\u25CB';
486
+ const display = m.alias ? `${m.alias} (${m.id})` : m.id;
487
+ lines.push(`${status} <code>${escapeHtml(display)}</code>${isCurrent ? ' \u2190 current' : ''}`);
488
+ const label = isCurrent ? `\u25CF ${m.alias || m.id}` : (m.alias || m.id);
489
+ rows.push([{ text: label, callback_data: `mod:${m.id}` }]);
490
+ }
491
+ await ctx.reply(lines.join('\n'), { parseMode: 'HTML', keyboard: { inline_keyboard: rows } });
492
+ }
457
493
  async cmdRestart(ctx) {
458
494
  const activeTasks = this.activeTasks.size;
459
495
  if (activeTasks > 0) {
@@ -598,6 +634,12 @@ export class TelegramBot extends Bot {
598
634
  tp.push(`cached: ${fmtTokens(result.cachedInputTokens)}`);
599
635
  if (result.outputTokens != null)
600
636
  tp.push(`out: ${fmtTokens(result.outputTokens)}`);
637
+ // Context window usage percentage (based on input tokens = full conversation context)
638
+ const ctxMax = getContextWindowSize(result.model);
639
+ if (ctxMax && result.inputTokens != null) {
640
+ const pct = (result.inputTokens / ctxMax * 100).toFixed(1);
641
+ tp.push(`ctx: ${pct}%`);
642
+ }
601
643
  tokenBlock = `\n<blockquote expandable>${tp.join(' ')}</blockquote>`;
602
644
  }
603
645
  const quickReplies = result.incomplete ? [] : detectQuickReplies(result.message);
@@ -760,6 +802,21 @@ export class TelegramBot extends Bot {
760
802
  await ctx.editReply(ctx.messageId, `<b>Switched to ${escapeHtml(agent)}</b>\n\nSession has been reset. Previous conversation history will not carry over.\nSend a message to start a new conversation.`, { parseMode: 'HTML' });
761
803
  return;
762
804
  }
805
+ if (data.startsWith('mod:')) {
806
+ const modelId = data.slice(4);
807
+ const cs = this.chat(ctx.chatId);
808
+ const currentModel = this.modelForAgent(cs.agent);
809
+ if (currentModel === modelId) {
810
+ await ctx.answerCallback(`Already using ${modelId}`);
811
+ return;
812
+ }
813
+ this.setModelForAgent(cs.agent, modelId);
814
+ cs.sessionId = null;
815
+ this.log(`model switched to ${modelId} for ${cs.agent} chat=${ctx.chatId}`);
816
+ await ctx.answerCallback(`Switched to ${modelId}`);
817
+ await ctx.editReply(ctx.messageId, `<b>Model switched to <code>${escapeHtml(modelId)}</code></b>\n\nAgent: ${escapeHtml(cs.agent)}\nSession has been reset. Send a message to start a new conversation.`, { parseMode: 'HTML' });
818
+ return;
819
+ }
763
820
  if (data.startsWith('qr:')) {
764
821
  const parts = data.split(':');
765
822
  if (parts.length === 3) {
@@ -788,6 +845,9 @@ export class TelegramBot extends Bot {
788
845
  case 'agents':
789
846
  await this.cmdAgents(ctx);
790
847
  return;
848
+ case 'models':
849
+ await this.cmdModels(ctx);
850
+ return;
791
851
  case 'status':
792
852
  await this.cmdStatus(ctx);
793
853
  return;
package/dist/bot.js CHANGED
@@ -7,8 +7,8 @@ import os from 'node:os';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { execSync, spawn } from 'node:child_process';
10
- import { doStream, getSessions, getUsage, listAgents, } from './code-agent.js';
11
- export const VERSION = '0.2.6';
10
+ import { doStream, getSessions, getUsage, listAgents, listModels, } from './code-agent.js';
11
+ export const VERSION = '0.2.8';
12
12
  // ---------------------------------------------------------------------------
13
13
  // Helpers
14
14
  // ---------------------------------------------------------------------------
@@ -186,6 +186,16 @@ export class Bot {
186
186
  fetchAgents() {
187
187
  return listAgents();
188
188
  }
189
+ fetchModels(agent) {
190
+ return listModels(agent);
191
+ }
192
+ setModelForAgent(agent, modelId) {
193
+ if (agent === 'codex')
194
+ this.codexModel = modelId;
195
+ else
196
+ this.claudeModel = modelId;
197
+ this.log(`model for ${agent} changed to ${modelId}`);
198
+ }
189
199
  getStatusData(chatId) {
190
200
  const cs = this.chat(chatId);
191
201
  const mem = process.memoryUsage();
@@ -528,6 +528,21 @@ export function listAgents() {
528
528
  ],
529
529
  };
530
530
  }
531
+ const CLAUDE_MODELS = [
532
+ { id: 'claude-opus-4-6', alias: 'opus' },
533
+ { id: 'claude-sonnet-4-5-20250929', alias: 'sonnet' },
534
+ { id: 'claude-haiku-4-5-20250929', alias: 'haiku' },
535
+ ];
536
+ const CODEX_MODELS = [
537
+ { id: 'o4-mini', alias: null },
538
+ { id: 'o3', alias: null },
539
+ { id: 'gpt-5.4', alias: null },
540
+ { id: 'gpt-4.1', alias: null },
541
+ { id: 'codex-mini', alias: null },
542
+ ];
543
+ export function listModels(agent) {
544
+ return { agent, models: agent === 'codex' ? CODEX_MODELS : CLAUDE_MODELS };
545
+ }
531
546
  function toIsoFromEpochSeconds(value) {
532
547
  const n = Number(value);
533
548
  if (!Number.isFinite(n) || n <= 0)
@@ -728,6 +743,92 @@ function getCodexUsageFromSessions(home) {
728
743
  }
729
744
  return null;
730
745
  }
746
+ // ---------------------------------------------------------------------------
747
+ // Claude usage from OAuth API (https://api.anthropic.com/api/oauth/usage)
748
+ // ---------------------------------------------------------------------------
749
+ function getClaudeOAuthToken() {
750
+ try {
751
+ const raw = execSync('security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null', {
752
+ encoding: 'utf-8', timeout: 3000,
753
+ }).trim();
754
+ if (!raw)
755
+ return null;
756
+ const parsed = JSON.parse(raw);
757
+ return parsed?.claudeAiOauth?.accessToken || null;
758
+ }
759
+ catch {
760
+ return null;
761
+ }
762
+ }
763
+ function getClaudeUsageFromOAuth() {
764
+ const token = getClaudeOAuthToken();
765
+ if (!token)
766
+ return null;
767
+ try {
768
+ const raw = execSync(`curl -s --max-time 5 -H "Authorization: Bearer ${token}" -H "anthropic-beta: oauth-2025-04-20" -H "Content-Type: application/json" "https://api.anthropic.com/api/oauth/usage"`, { encoding: 'utf-8', timeout: 8000 }).trim();
769
+ if (!raw || raw[0] !== '{')
770
+ return null;
771
+ const data = JSON.parse(raw);
772
+ const capturedAt = new Date().toISOString();
773
+ const makeWindow = (label, entry) => {
774
+ if (!entry || typeof entry !== 'object')
775
+ return null;
776
+ const usedPercent = roundPercent(entry.utilization);
777
+ if (usedPercent == null)
778
+ return null;
779
+ const remainingPercent = Math.max(0, Math.round((100 - usedPercent) * 10) / 10);
780
+ const resetAt = typeof entry.resets_at === 'string' ? entry.resets_at : null;
781
+ let resetAfterSeconds = null;
782
+ if (resetAt) {
783
+ const resetAtMs = Date.parse(resetAt);
784
+ if (Number.isFinite(resetAtMs)) {
785
+ resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
786
+ }
787
+ }
788
+ return {
789
+ label,
790
+ usedPercent,
791
+ remainingPercent,
792
+ resetAt,
793
+ resetAfterSeconds,
794
+ status: usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed',
795
+ };
796
+ };
797
+ const windows = [];
798
+ const w5h = makeWindow('5h', data.five_hour);
799
+ if (w5h)
800
+ windows.push(w5h);
801
+ const w7d = makeWindow('7d', data.seven_day);
802
+ if (w7d)
803
+ windows.push(w7d);
804
+ const w7dOpus = makeWindow('7d Opus', data.seven_day_opus);
805
+ if (w7dOpus)
806
+ windows.push(w7dOpus);
807
+ const w7dSonnet = makeWindow('7d Sonnet', data.seven_day_sonnet);
808
+ if (w7dSonnet)
809
+ windows.push(w7dSonnet);
810
+ const wExtra = makeWindow('Extra', data.extra_usage);
811
+ if (wExtra)
812
+ windows.push(wExtra);
813
+ if (!windows.length)
814
+ return null;
815
+ const overallStatus = windows.some(w => w.status === 'limit_reached') ? 'limit_reached'
816
+ : windows.some(w => w.status === 'warning') ? 'warning'
817
+ : 'allowed';
818
+ return {
819
+ ok: true,
820
+ agent: 'claude',
821
+ source: 'oauth-api',
822
+ capturedAt,
823
+ status: overallStatus,
824
+ windows,
825
+ error: null,
826
+ };
827
+ }
828
+ catch {
829
+ return null;
830
+ }
831
+ }
731
832
  function getClaudeUsageFromTelemetry(home, model) {
732
833
  const telemetryRoot = path.join(home, '.claude', 'telemetry');
733
834
  if (!fs.existsSync(telemetryRoot))
@@ -821,6 +922,7 @@ export function getUsage(opts) {
821
922
  || getCodexUsageFromSessions(home)
822
923
  || emptyUsage('codex', 'No recent Codex usage data found.');
823
924
  }
824
- return getClaudeUsageFromTelemetry(home, opts.model)
925
+ return getClaudeUsageFromOAuth()
926
+ || getClaudeUsageFromTelemetry(home, opts.model)
825
927
  || emptyUsage('claude', 'No recent Claude usage data found.');
826
928
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeclaw",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "The best IM-driven remote coding experience. Bridge AI coding agents to any IM.",
5
5
  "type": "module",
6
6
  "bin": {