codeclaw 0.2.5 → 0.2.7

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.
@@ -10,6 +10,7 @@ import path from 'node:path';
10
10
  import { spawn } from 'node:child_process';
11
11
  import { Bot, VERSION, fmtTokens, fmtUptime, fmtBytes, whichSync, listSubdirs, buildPrompt, thinkLabel, parseAllowedChatIds, shellSplit, } from './bot.js';
12
12
  import { TelegramChannel } from './channel-telegram.js';
13
+ import { splitText } from './channel-base.js';
13
14
  // ---------------------------------------------------------------------------
14
15
  // Telegram HTML formatting
15
16
  // ---------------------------------------------------------------------------
@@ -657,20 +658,39 @@ export class TelegramBot extends Bot {
657
658
  }
658
659
  }
659
660
  else {
660
- let preview = bodyHtml.slice(0, 3200);
661
- if (bodyHtml.length > 3200)
662
- preview += '\n<i>... (see full response below)</i>';
663
- const previewHtml = `${statusHtml}${thinkingHtml}${preview}\n\n${meta}${tokenBlock}`;
661
+ // Send full content as split plain-text messages instead of a file.
662
+ // First message: edit placeholder with meta + thinking + beginning of body.
663
+ const headerHtml = `${statusHtml}${thinkingHtml}`;
664
+ const footerHtml = `\n\n${meta}${tokenBlock}`;
665
+ const maxFirst = 3900 - headerHtml.length - footerHtml.length;
666
+ let firstBody;
667
+ let remaining;
668
+ if (maxFirst > 200) {
669
+ // find a newline-friendly cut in the HTML body
670
+ let cut = bodyHtml.lastIndexOf('\n', maxFirst);
671
+ if (cut < maxFirst * 0.3)
672
+ cut = maxFirst;
673
+ firstBody = bodyHtml.slice(0, cut);
674
+ remaining = bodyHtml.slice(cut);
675
+ }
676
+ else {
677
+ firstBody = '';
678
+ remaining = bodyHtml;
679
+ }
680
+ const firstHtml = `${headerHtml}${firstBody}${footerHtml}`;
664
681
  try {
665
- await this.channel.editMessage(ctx.chatId, phId, previewHtml, { parseMode: 'HTML', keyboard });
682
+ await this.channel.editMessage(ctx.chatId, phId, firstHtml, { parseMode: 'HTML', keyboard });
666
683
  }
667
684
  catch {
668
- finalMsgId = await this.channel.send(ctx.chatId, previewHtml, { parseMode: 'HTML', replyTo: ctx.messageId, keyboard });
685
+ finalMsgId = await this.channel.send(ctx.chatId, firstHtml, { parseMode: 'HTML', replyTo: ctx.messageId, keyboard });
686
+ }
687
+ // Send remaining body as continuation messages (split at ~3800 chars)
688
+ if (remaining.trim()) {
689
+ const chunks = splitText(remaining, 3800);
690
+ for (const chunk of chunks) {
691
+ await this.channel.send(ctx.chatId, chunk, { parseMode: 'HTML', replyTo: finalMsgId ?? phId });
692
+ }
669
693
  }
670
- const thinkingMd = result.thinking
671
- ? `> **${thinkLabel(agent)}**\n${result.thinking.split('\n').map(l => `> ${l}`).join('\n')}\n\n---\n\n`
672
- : '';
673
- await this.channel.sendDocument(ctx.chatId, thinkingMd + result.message, `response_${phId}.md`, { caption: `Full response (${result.message.length} chars)`, replyTo: finalMsgId ?? phId });
674
694
  }
675
695
  return finalMsgId;
676
696
  }
package/dist/bot.js CHANGED
@@ -8,7 +8,7 @@ import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { execSync, spawn } from 'node:child_process';
10
10
  import { doStream, getSessions, getUsage, listAgents, } from './code-agent.js';
11
- export const VERSION = '0.2.5';
11
+ export const VERSION = '0.2.7';
12
12
  // ---------------------------------------------------------------------------
13
13
  // Helpers
14
14
  // ---------------------------------------------------------------------------
@@ -25,7 +25,7 @@ async function run(cmd, opts, parseLine) {
25
25
  const proc = spawn(shellCmd, { cwd: opts.workdir, stdio: ['pipe', 'pipe', 'pipe'], shell: true });
26
26
  agentLog(`[spawn] pid=${proc.pid}`);
27
27
  try {
28
- proc.stdin.write(opts.prompt);
28
+ proc.stdin.write(opts._stdinOverride ?? opts.prompt);
29
29
  proc.stdin.end();
30
30
  }
31
31
  catch { }
@@ -147,6 +147,49 @@ export function doCodexStream(opts) {
147
147
  return run(codexCmd(opts), opts, codexParse);
148
148
  }
149
149
  // --- claude ---
150
+ const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp']);
151
+ function mimeForExt(ext) {
152
+ switch (ext) {
153
+ case '.jpg':
154
+ case '.jpeg': return 'image/jpeg';
155
+ case '.png': return 'image/png';
156
+ case '.gif': return 'image/gif';
157
+ case '.webp': return 'image/webp';
158
+ default: return 'application/octet-stream';
159
+ }
160
+ }
161
+ /**
162
+ * Build a stream-json stdin payload that includes images as base64 content
163
+ * blocks alongside the text prompt.
164
+ */
165
+ function buildClaudeMultimodalStdin(prompt, attachments) {
166
+ const content = [];
167
+ for (const filePath of attachments) {
168
+ const ext = path.extname(filePath).toLowerCase();
169
+ if (IMAGE_EXTS.has(ext)) {
170
+ try {
171
+ const data = fs.readFileSync(filePath);
172
+ content.push({
173
+ type: 'image',
174
+ source: { type: 'base64', media_type: mimeForExt(ext), data: data.toString('base64') },
175
+ });
176
+ }
177
+ catch (e) {
178
+ agentLog(`[attach] failed to read image ${filePath}: ${e.message}`);
179
+ }
180
+ }
181
+ else {
182
+ // For non-image files, tell Claude the path so it can Read it
183
+ content.push({ type: 'text', text: `[Attached file: ${filePath}]` });
184
+ }
185
+ }
186
+ content.push({ type: 'text', text: prompt });
187
+ const msg = {
188
+ type: 'user',
189
+ message: { role: 'user', content },
190
+ };
191
+ return JSON.stringify(msg) + '\n';
192
+ }
150
193
  function claudeCmd(o) {
151
194
  const args = ['claude', '-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
152
195
  if (o.claudeModel)
@@ -156,8 +199,8 @@ function claudeCmd(o) {
156
199
  if (o.sessionId)
157
200
  args.push('--resume', o.sessionId);
158
201
  if (o.attachments?.length) {
159
- for (const f of o.attachments)
160
- args.push('--input-file', f);
202
+ args.push('--input-format', 'stream-json');
203
+ o._stdinOverride = buildClaudeMultimodalStdin(o.prompt, o.attachments);
161
204
  }
162
205
  if (o.claudeExtraArgs?.length)
163
206
  args.push(...o.claudeExtraArgs);
@@ -685,6 +728,92 @@ function getCodexUsageFromSessions(home) {
685
728
  }
686
729
  return null;
687
730
  }
731
+ // ---------------------------------------------------------------------------
732
+ // Claude usage from OAuth API (https://api.anthropic.com/api/oauth/usage)
733
+ // ---------------------------------------------------------------------------
734
+ function getClaudeOAuthToken() {
735
+ try {
736
+ const raw = execSync('security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null', {
737
+ encoding: 'utf-8', timeout: 3000,
738
+ }).trim();
739
+ if (!raw)
740
+ return null;
741
+ const parsed = JSON.parse(raw);
742
+ return parsed?.claudeAiOauth?.accessToken || null;
743
+ }
744
+ catch {
745
+ return null;
746
+ }
747
+ }
748
+ function getClaudeUsageFromOAuth() {
749
+ const token = getClaudeOAuthToken();
750
+ if (!token)
751
+ return null;
752
+ try {
753
+ 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();
754
+ if (!raw || raw[0] !== '{')
755
+ return null;
756
+ const data = JSON.parse(raw);
757
+ const capturedAt = new Date().toISOString();
758
+ const makeWindow = (label, entry) => {
759
+ if (!entry || typeof entry !== 'object')
760
+ return null;
761
+ const usedPercent = roundPercent(entry.utilization);
762
+ if (usedPercent == null)
763
+ return null;
764
+ const remainingPercent = Math.max(0, Math.round((100 - usedPercent) * 10) / 10);
765
+ const resetAt = typeof entry.resets_at === 'string' ? entry.resets_at : null;
766
+ let resetAfterSeconds = null;
767
+ if (resetAt) {
768
+ const resetAtMs = Date.parse(resetAt);
769
+ if (Number.isFinite(resetAtMs)) {
770
+ resetAfterSeconds = Math.max(0, Math.round((resetAtMs - Date.now()) / 1000));
771
+ }
772
+ }
773
+ return {
774
+ label,
775
+ usedPercent,
776
+ remainingPercent,
777
+ resetAt,
778
+ resetAfterSeconds,
779
+ status: usedPercent >= 100 ? 'limit_reached' : usedPercent >= 80 ? 'warning' : 'allowed',
780
+ };
781
+ };
782
+ const windows = [];
783
+ const w5h = makeWindow('5h', data.five_hour);
784
+ if (w5h)
785
+ windows.push(w5h);
786
+ const w7d = makeWindow('7d', data.seven_day);
787
+ if (w7d)
788
+ windows.push(w7d);
789
+ const w7dOpus = makeWindow('7d Opus', data.seven_day_opus);
790
+ if (w7dOpus)
791
+ windows.push(w7dOpus);
792
+ const w7dSonnet = makeWindow('7d Sonnet', data.seven_day_sonnet);
793
+ if (w7dSonnet)
794
+ windows.push(w7dSonnet);
795
+ const wExtra = makeWindow('Extra', data.extra_usage);
796
+ if (wExtra)
797
+ windows.push(wExtra);
798
+ if (!windows.length)
799
+ return null;
800
+ const overallStatus = windows.some(w => w.status === 'limit_reached') ? 'limit_reached'
801
+ : windows.some(w => w.status === 'warning') ? 'warning'
802
+ : 'allowed';
803
+ return {
804
+ ok: true,
805
+ agent: 'claude',
806
+ source: 'oauth-api',
807
+ capturedAt,
808
+ status: overallStatus,
809
+ windows,
810
+ error: null,
811
+ };
812
+ }
813
+ catch {
814
+ return null;
815
+ }
816
+ }
688
817
  function getClaudeUsageFromTelemetry(home, model) {
689
818
  const telemetryRoot = path.join(home, '.claude', 'telemetry');
690
819
  if (!fs.existsSync(telemetryRoot))
@@ -778,6 +907,7 @@ export function getUsage(opts) {
778
907
  || getCodexUsageFromSessions(home)
779
908
  || emptyUsage('codex', 'No recent Codex usage data found.');
780
909
  }
781
- return getClaudeUsageFromTelemetry(home, opts.model)
910
+ return getClaudeUsageFromOAuth()
911
+ || getClaudeUsageFromTelemetry(home, opts.model)
782
912
  || emptyUsage('claude', 'No recent Claude usage data found.');
783
913
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeclaw",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "The best IM-driven remote coding experience. Bridge AI coding agents to any IM.",
5
5
  "type": "module",
6
6
  "bin": {