codeclaw 0.2.4 → 0.2.6
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/dist/bot-telegram.js +102 -73
- package/dist/bot.js +5 -3
- package/dist/code-agent.js +46 -3
- package/package.json +1 -1
package/dist/bot-telegram.js
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -105,7 +106,72 @@ const ARTIFACT_PHOTO_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
|
|
|
105
106
|
function isPhotoFilename(filename) {
|
|
106
107
|
return ARTIFACT_PHOTO_EXTS.has(path.extname(filename).toLowerCase());
|
|
107
108
|
}
|
|
108
|
-
function
|
|
109
|
+
export function collectArtifacts(dirPath, manifestPath, log) {
|
|
110
|
+
const _log = log || (() => { });
|
|
111
|
+
if (!fs.existsSync(manifestPath))
|
|
112
|
+
return [];
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
_log(`artifact manifest parse error: ${e}`);
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.files) ? parsed.files : [];
|
|
122
|
+
if (!entries.length)
|
|
123
|
+
return [];
|
|
124
|
+
const realDir = fs.realpathSync(dirPath);
|
|
125
|
+
const artifacts = [];
|
|
126
|
+
for (const entry of entries.slice(0, ARTIFACT_MAX_FILES)) {
|
|
127
|
+
const rawPath = typeof entry?.path === 'string' ? entry.path
|
|
128
|
+
: typeof entry?.name === 'string' ? entry.name
|
|
129
|
+
: '';
|
|
130
|
+
const relPath = rawPath.trim();
|
|
131
|
+
if (!relPath || path.isAbsolute(relPath)) {
|
|
132
|
+
_log(`artifact skipped: invalid path "${rawPath}"`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const resolved = path.resolve(dirPath, relPath);
|
|
136
|
+
const relative = path.relative(dirPath, resolved);
|
|
137
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
138
|
+
_log(`artifact skipped: outside turn dir "${relPath}"`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!fs.existsSync(resolved)) {
|
|
142
|
+
_log(`artifact skipped: missing file "${relPath}"`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const realFile = fs.realpathSync(resolved);
|
|
146
|
+
const realRelative = path.relative(realDir, realFile);
|
|
147
|
+
if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
|
|
148
|
+
_log(`artifact skipped: symlink outside turn dir "${relPath}"`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const stat = fs.statSync(realFile);
|
|
152
|
+
if (!stat.isFile()) {
|
|
153
|
+
_log(`artifact skipped: not a file "${relPath}"`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (stat.size > ARTIFACT_MAX_BYTES) {
|
|
157
|
+
_log(`artifact skipped: too large "${relPath}" (${stat.size} bytes)`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const filename = path.basename(realFile);
|
|
161
|
+
const requestedKind = typeof entry?.kind === 'string' ? entry.kind.toLowerCase()
|
|
162
|
+
: typeof entry?.type === 'string' ? entry.type.toLowerCase()
|
|
163
|
+
: '';
|
|
164
|
+
let kind = requestedKind === 'document' ? 'document'
|
|
165
|
+
: requestedKind === 'photo' ? 'photo'
|
|
166
|
+
: isPhotoFilename(filename) ? 'photo' : 'document';
|
|
167
|
+
if (kind === 'photo' && !isPhotoFilename(filename))
|
|
168
|
+
kind = 'document';
|
|
169
|
+
const caption = typeof entry?.caption === 'string' ? entry.caption.trim().slice(0, 1024) || undefined : undefined;
|
|
170
|
+
artifacts.push({ filePath: realFile, filename, kind, caption });
|
|
171
|
+
}
|
|
172
|
+
return artifacts;
|
|
173
|
+
}
|
|
174
|
+
export function buildArtifactPrompt(prompt, artifactDir, manifestPath) {
|
|
109
175
|
const base = prompt.trim() || 'Please help with this request.';
|
|
110
176
|
return [
|
|
111
177
|
base,
|
|
@@ -409,12 +475,17 @@ export class TelegramBot extends Bot {
|
|
|
409
475
|
const baseArgs = ensureNonInteractiveRestartArgs(bin, rawArgs);
|
|
410
476
|
const allArgs = [...baseArgs, ...process.argv.slice(2)];
|
|
411
477
|
this.log(`restart: spawning \`${bin} ${allArgs.join(' ')}\``);
|
|
478
|
+
// Collect all known chat IDs so the new process can send startup notices
|
|
479
|
+
const knownIds = new Set(this.allowedChatIds);
|
|
480
|
+
for (const cid of this.channel.knownChats)
|
|
481
|
+
knownIds.add(cid);
|
|
412
482
|
const child = spawn(bin, allArgs, {
|
|
413
483
|
stdio: 'inherit',
|
|
414
484
|
detached: true,
|
|
415
485
|
env: {
|
|
416
486
|
...process.env,
|
|
417
487
|
npm_config_yes: process.env.npm_config_yes || 'true',
|
|
488
|
+
...(knownIds.size ? { TELEGRAM_ALLOWED_CHAT_IDS: [...knownIds].join(',') } : {}),
|
|
418
489
|
},
|
|
419
490
|
});
|
|
420
491
|
child.unref();
|
|
@@ -494,68 +565,7 @@ export class TelegramBot extends Bot {
|
|
|
494
565
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
495
566
|
}
|
|
496
567
|
collectArtifacts(dirPath, manifestPath) {
|
|
497
|
-
|
|
498
|
-
return [];
|
|
499
|
-
let parsed;
|
|
500
|
-
try {
|
|
501
|
-
parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
502
|
-
}
|
|
503
|
-
catch (e) {
|
|
504
|
-
this.log(`artifact manifest parse error: ${e}`);
|
|
505
|
-
return [];
|
|
506
|
-
}
|
|
507
|
-
const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.files) ? parsed.files : [];
|
|
508
|
-
if (!entries.length)
|
|
509
|
-
return [];
|
|
510
|
-
const realDir = fs.realpathSync(dirPath);
|
|
511
|
-
const artifacts = [];
|
|
512
|
-
for (const entry of entries.slice(0, ARTIFACT_MAX_FILES)) {
|
|
513
|
-
const rawPath = typeof entry?.path === 'string' ? entry.path
|
|
514
|
-
: typeof entry?.name === 'string' ? entry.name
|
|
515
|
-
: '';
|
|
516
|
-
const relPath = rawPath.trim();
|
|
517
|
-
if (!relPath || path.isAbsolute(relPath)) {
|
|
518
|
-
this.log(`artifact skipped: invalid path "${rawPath}"`);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
const resolved = path.resolve(dirPath, relPath);
|
|
522
|
-
const relative = path.relative(dirPath, resolved);
|
|
523
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
524
|
-
this.log(`artifact skipped: outside turn dir "${relPath}"`);
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
if (!fs.existsSync(resolved)) {
|
|
528
|
-
this.log(`artifact skipped: missing file "${relPath}"`);
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
const realFile = fs.realpathSync(resolved);
|
|
532
|
-
const realRelative = path.relative(realDir, realFile);
|
|
533
|
-
if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
|
|
534
|
-
this.log(`artifact skipped: symlink outside turn dir "${relPath}"`);
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
const stat = fs.statSync(realFile);
|
|
538
|
-
if (!stat.isFile()) {
|
|
539
|
-
this.log(`artifact skipped: not a file "${relPath}"`);
|
|
540
|
-
continue;
|
|
541
|
-
}
|
|
542
|
-
if (stat.size > ARTIFACT_MAX_BYTES) {
|
|
543
|
-
this.log(`artifact skipped: too large "${relPath}" (${stat.size} bytes)`);
|
|
544
|
-
continue;
|
|
545
|
-
}
|
|
546
|
-
const filename = path.basename(realFile);
|
|
547
|
-
const requestedKind = typeof entry?.kind === 'string' ? entry.kind.toLowerCase()
|
|
548
|
-
: typeof entry?.type === 'string' ? entry.type.toLowerCase()
|
|
549
|
-
: '';
|
|
550
|
-
let kind = requestedKind === 'document' ? 'document'
|
|
551
|
-
: requestedKind === 'photo' ? 'photo'
|
|
552
|
-
: isPhotoFilename(filename) ? 'photo' : 'document';
|
|
553
|
-
if (kind === 'photo' && !isPhotoFilename(filename))
|
|
554
|
-
kind = 'document';
|
|
555
|
-
const caption = typeof entry?.caption === 'string' ? entry.caption.trim().slice(0, 1024) || undefined : undefined;
|
|
556
|
-
artifacts.push({ filePath: realFile, filename, kind, caption });
|
|
557
|
-
}
|
|
558
|
-
return artifacts;
|
|
568
|
+
return collectArtifacts(dirPath, manifestPath, msg => this.log(msg));
|
|
559
569
|
}
|
|
560
570
|
async sendArtifacts(ctx, replyTo, artifacts) {
|
|
561
571
|
for (const artifact of artifacts) {
|
|
@@ -648,20 +658,39 @@ export class TelegramBot extends Bot {
|
|
|
648
658
|
}
|
|
649
659
|
}
|
|
650
660
|
else {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const
|
|
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}`;
|
|
655
681
|
try {
|
|
656
|
-
await this.channel.editMessage(ctx.chatId, phId,
|
|
682
|
+
await this.channel.editMessage(ctx.chatId, phId, firstHtml, { parseMode: 'HTML', keyboard });
|
|
657
683
|
}
|
|
658
684
|
catch {
|
|
659
|
-
finalMsgId = await this.channel.send(ctx.chatId,
|
|
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
|
+
}
|
|
660
693
|
}
|
|
661
|
-
const thinkingMd = result.thinking
|
|
662
|
-
? `> **${thinkLabel(agent)}**\n${result.thinking.split('\n').map(l => `> ${l}`).join('\n')}\n\n---\n\n`
|
|
663
|
-
: '';
|
|
664
|
-
await this.channel.sendDocument(ctx.chatId, thinkingMd + result.message, `response_${phId}.md`, { caption: `Full response (${result.message.length} chars)`, replyTo: finalMsgId ?? phId });
|
|
665
694
|
}
|
|
666
695
|
return finalMsgId;
|
|
667
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.
|
|
11
|
+
export const VERSION = '0.2.6';
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Helpers
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
@@ -235,9 +235,10 @@ export class Bot {
|
|
|
235
235
|
else if (cs.agent === 'codex') {
|
|
236
236
|
this.log(`[runStream] codex config: model=${this.codexModel} reasoning=${this.codexReasoningEffort} fullAccess=${this.codexFullAccess} extraArgs=[${this.codexExtraArgs.join(' ')}]`);
|
|
237
237
|
}
|
|
238
|
+
const snapshotSessionId = cs.sessionId;
|
|
238
239
|
const opts = {
|
|
239
240
|
agent: cs.agent, prompt, workdir: this.workdir, timeout: this.runTimeout,
|
|
240
|
-
sessionId:
|
|
241
|
+
sessionId: snapshotSessionId, model: null, thinkingEffort: this.codexReasoningEffort, onText,
|
|
241
242
|
attachments: attachments.length ? attachments : undefined,
|
|
242
243
|
codexModel: this.codexModel, codexFullAccess: this.codexFullAccess,
|
|
243
244
|
codexExtraArgs: this.codexExtraArgs.length ? this.codexExtraArgs : undefined,
|
|
@@ -252,7 +253,8 @@ export class Bot {
|
|
|
252
253
|
this.stats.totalOutputTokens += result.outputTokens;
|
|
253
254
|
if (result.cachedInputTokens)
|
|
254
255
|
this.stats.totalCachedTokens += result.cachedInputTokens;
|
|
255
|
-
if (
|
|
256
|
+
// Only update sessionId if it hasn't been changed externally (e.g. user switched session during run)
|
|
257
|
+
if (result.sessionId && cs.sessionId === snapshotSessionId)
|
|
256
258
|
cs.sessionId = result.sessionId;
|
|
257
259
|
this.log(`[runStream] completed turn=${this.stats.totalTurns} cumulative: in=${fmtTokens(this.stats.totalInputTokens)} out=${fmtTokens(this.stats.totalOutputTokens)} cached=${fmtTokens(this.stats.totalCachedTokens)}`);
|
|
258
260
|
return result;
|
package/dist/code-agent.js
CHANGED
|
@@ -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
|
-
|
|
160
|
-
|
|
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);
|