codeclaw 0.2.4 → 0.2.5

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.
@@ -105,7 +105,72 @@ const ARTIFACT_PHOTO_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
105
105
  function isPhotoFilename(filename) {
106
106
  return ARTIFACT_PHOTO_EXTS.has(path.extname(filename).toLowerCase());
107
107
  }
108
- function buildArtifactPrompt(prompt, artifactDir, manifestPath) {
108
+ export function collectArtifacts(dirPath, manifestPath, log) {
109
+ const _log = log || (() => { });
110
+ if (!fs.existsSync(manifestPath))
111
+ return [];
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
115
+ }
116
+ catch (e) {
117
+ _log(`artifact manifest parse error: ${e}`);
118
+ return [];
119
+ }
120
+ const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.files) ? parsed.files : [];
121
+ if (!entries.length)
122
+ return [];
123
+ const realDir = fs.realpathSync(dirPath);
124
+ const artifacts = [];
125
+ for (const entry of entries.slice(0, ARTIFACT_MAX_FILES)) {
126
+ const rawPath = typeof entry?.path === 'string' ? entry.path
127
+ : typeof entry?.name === 'string' ? entry.name
128
+ : '';
129
+ const relPath = rawPath.trim();
130
+ if (!relPath || path.isAbsolute(relPath)) {
131
+ _log(`artifact skipped: invalid path "${rawPath}"`);
132
+ continue;
133
+ }
134
+ const resolved = path.resolve(dirPath, relPath);
135
+ const relative = path.relative(dirPath, resolved);
136
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
137
+ _log(`artifact skipped: outside turn dir "${relPath}"`);
138
+ continue;
139
+ }
140
+ if (!fs.existsSync(resolved)) {
141
+ _log(`artifact skipped: missing file "${relPath}"`);
142
+ continue;
143
+ }
144
+ const realFile = fs.realpathSync(resolved);
145
+ const realRelative = path.relative(realDir, realFile);
146
+ if (realRelative.startsWith('..') || path.isAbsolute(realRelative)) {
147
+ _log(`artifact skipped: symlink outside turn dir "${relPath}"`);
148
+ continue;
149
+ }
150
+ const stat = fs.statSync(realFile);
151
+ if (!stat.isFile()) {
152
+ _log(`artifact skipped: not a file "${relPath}"`);
153
+ continue;
154
+ }
155
+ if (stat.size > ARTIFACT_MAX_BYTES) {
156
+ _log(`artifact skipped: too large "${relPath}" (${stat.size} bytes)`);
157
+ continue;
158
+ }
159
+ const filename = path.basename(realFile);
160
+ const requestedKind = typeof entry?.kind === 'string' ? entry.kind.toLowerCase()
161
+ : typeof entry?.type === 'string' ? entry.type.toLowerCase()
162
+ : '';
163
+ let kind = requestedKind === 'document' ? 'document'
164
+ : requestedKind === 'photo' ? 'photo'
165
+ : isPhotoFilename(filename) ? 'photo' : 'document';
166
+ if (kind === 'photo' && !isPhotoFilename(filename))
167
+ kind = 'document';
168
+ const caption = typeof entry?.caption === 'string' ? entry.caption.trim().slice(0, 1024) || undefined : undefined;
169
+ artifacts.push({ filePath: realFile, filename, kind, caption });
170
+ }
171
+ return artifacts;
172
+ }
173
+ export function buildArtifactPrompt(prompt, artifactDir, manifestPath) {
109
174
  const base = prompt.trim() || 'Please help with this request.';
110
175
  return [
111
176
  base,
@@ -409,12 +474,17 @@ export class TelegramBot extends Bot {
409
474
  const baseArgs = ensureNonInteractiveRestartArgs(bin, rawArgs);
410
475
  const allArgs = [...baseArgs, ...process.argv.slice(2)];
411
476
  this.log(`restart: spawning \`${bin} ${allArgs.join(' ')}\``);
477
+ // Collect all known chat IDs so the new process can send startup notices
478
+ const knownIds = new Set(this.allowedChatIds);
479
+ for (const cid of this.channel.knownChats)
480
+ knownIds.add(cid);
412
481
  const child = spawn(bin, allArgs, {
413
482
  stdio: 'inherit',
414
483
  detached: true,
415
484
  env: {
416
485
  ...process.env,
417
486
  npm_config_yes: process.env.npm_config_yes || 'true',
487
+ ...(knownIds.size ? { TELEGRAM_ALLOWED_CHAT_IDS: [...knownIds].join(',') } : {}),
418
488
  },
419
489
  });
420
490
  child.unref();
@@ -494,68 +564,7 @@ export class TelegramBot extends Bot {
494
564
  fs.rmSync(dirPath, { recursive: true, force: true });
495
565
  }
496
566
  collectArtifacts(dirPath, manifestPath) {
497
- if (!fs.existsSync(manifestPath))
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;
567
+ return collectArtifacts(dirPath, manifestPath, msg => this.log(msg));
559
568
  }
560
569
  async sendArtifacts(ctx, replyTo, artifacts) {
561
570
  for (const artifact of artifacts) {
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.4';
11
+ export const VERSION = '0.2.5';
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: cs.sessionId, model: null, thinkingEffort: this.codexReasoningEffort, onText,
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 (result.sessionId)
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeclaw",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "The best IM-driven remote coding experience. Bridge AI coding agents to any IM.",
5
5
  "type": "module",
6
6
  "bin": {