codeclaw 0.1.0 → 0.2.2
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 +165 -170
- package/dist/bot-telegram.js +652 -0
- package/dist/bot.js +288 -0
- package/dist/channel-base.js +30 -0
- package/dist/channel-telegram.js +474 -0
- package/dist/cli.js +209 -0
- package/dist/code-agent.js +472 -0
- package/package.json +8 -4
- package/bin/codeclaw.js +0 -3
- package/src/channel-telegram.js +0 -1070
- package/src/codeclaw.js +0 -781
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { execSync, spawn } from 'node:child_process';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const Q = (a) => /[^a-zA-Z0-9_./:=@-]/.test(a) ? `'${a.replace(/'/g, "'\\''")}'` : a;
|
|
6
|
+
function agentLog(msg) {
|
|
7
|
+
const ts = new Date().toTimeString().slice(0, 8);
|
|
8
|
+
process.stdout.write(`[agent ${ts}] ${msg}\n`);
|
|
9
|
+
}
|
|
10
|
+
async function run(cmd, opts, parseLine) {
|
|
11
|
+
const start = Date.now();
|
|
12
|
+
const deadline = start + opts.timeout * 1000;
|
|
13
|
+
let stderr = '';
|
|
14
|
+
let lineCount = 0;
|
|
15
|
+
const s = {
|
|
16
|
+
sessionId: opts.sessionId, text: '', thinking: '', msgs: [], thinkParts: [],
|
|
17
|
+
model: opts.model, thinkingEffort: opts.thinkingEffort, errors: null,
|
|
18
|
+
inputTokens: null, outputTokens: null, cachedInputTokens: null,
|
|
19
|
+
};
|
|
20
|
+
const shellCmd = cmd.map(Q).join(' ');
|
|
21
|
+
agentLog(`[spawn] cmd: ${shellCmd}`);
|
|
22
|
+
agentLog(`[spawn] cwd: ${opts.workdir} timeout: ${opts.timeout}s session: ${opts.sessionId || '(new)'}`);
|
|
23
|
+
agentLog(`[spawn] prompt: "${opts.prompt.slice(0, 120)}"`);
|
|
24
|
+
const proc = spawn(shellCmd, { cwd: opts.workdir, stdio: ['pipe', 'pipe', 'pipe'], shell: true });
|
|
25
|
+
agentLog(`[spawn] pid=${proc.pid}`);
|
|
26
|
+
try {
|
|
27
|
+
proc.stdin.write(opts.prompt);
|
|
28
|
+
proc.stdin.end();
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
proc.stderr?.on('data', (c) => {
|
|
32
|
+
const chunk = c.toString();
|
|
33
|
+
stderr += chunk;
|
|
34
|
+
agentLog(`[stderr] ${chunk.trim().slice(0, 200)}`);
|
|
35
|
+
});
|
|
36
|
+
const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
37
|
+
rl.on('line', raw => {
|
|
38
|
+
if (Date.now() > deadline) {
|
|
39
|
+
agentLog(`[timeout] deadline exceeded, killing process`);
|
|
40
|
+
proc.kill('SIGKILL');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const line = raw.trim();
|
|
44
|
+
if (!line || line[0] !== '{')
|
|
45
|
+
return;
|
|
46
|
+
lineCount++;
|
|
47
|
+
try {
|
|
48
|
+
const ev = JSON.parse(line);
|
|
49
|
+
const evType = ev.type || '?';
|
|
50
|
+
// Log key events (not every delta to avoid spam)
|
|
51
|
+
if (evType === 'system' || evType === 'result' || evType === 'assistant' ||
|
|
52
|
+
evType === 'thread.started' || evType === 'turn.completed' || evType === 'item.completed') {
|
|
53
|
+
agentLog(`[event] type=${evType} session=${ev.session_id || s.sessionId || '?'} model=${ev.model || s.model || '?'}`);
|
|
54
|
+
}
|
|
55
|
+
if (evType === 'stream_event') {
|
|
56
|
+
const inner = ev.event || {};
|
|
57
|
+
if (inner.type === 'message_start' || inner.type === 'message_delta') {
|
|
58
|
+
agentLog(`[event] stream_event/${inner.type} session=${ev.session_id || '?'}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
parseLine(ev, s);
|
|
62
|
+
opts.onText(s.text, s.thinking);
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
});
|
|
66
|
+
const [ok, code] = await new Promise(resolve => {
|
|
67
|
+
proc.on('close', code => { agentLog(`[exit] code=${code} lines_parsed=${lineCount}`); resolve([code === 0, code]); });
|
|
68
|
+
proc.on('error', e => { agentLog(`[error] ${e.message}`); stderr += e.message; resolve([false, -1]); });
|
|
69
|
+
});
|
|
70
|
+
if (!s.text.trim() && s.msgs.length)
|
|
71
|
+
s.text = s.msgs.join('\n\n');
|
|
72
|
+
if (!s.thinking.trim() && s.thinkParts.length)
|
|
73
|
+
s.thinking = s.thinkParts.join('\n\n');
|
|
74
|
+
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
|
75
|
+
agentLog(`[result] ok=${ok && !s.errors} elapsed=${elapsed}s text=${s.text.length}chars thinking=${s.thinking.length}chars session=${s.sessionId || '?'}`);
|
|
76
|
+
if (s.errors)
|
|
77
|
+
agentLog(`[result] errors: ${s.errors.join('; ')}`);
|
|
78
|
+
if (stderr.trim() && !ok)
|
|
79
|
+
agentLog(`[result] stderr: ${stderr.trim().slice(0, 300)}`);
|
|
80
|
+
return {
|
|
81
|
+
ok: ok && !s.errors, sessionId: s.sessionId, model: s.model, thinkingEffort: s.thinkingEffort,
|
|
82
|
+
message: s.errors?.join('; ') || s.text.trim() || (ok ? '(no textual response)' : `Failed (exit=${code}).\n\n${stderr.trim() || '(no output)'}`),
|
|
83
|
+
thinking: s.thinking.trim() || null,
|
|
84
|
+
elapsedS: (Date.now() - start) / 1000,
|
|
85
|
+
inputTokens: s.inputTokens, outputTokens: s.outputTokens, cachedInputTokens: s.cachedInputTokens,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// --- codex ---
|
|
89
|
+
function codexCmd(o) {
|
|
90
|
+
const args = ['codex', 'exec'];
|
|
91
|
+
if (o.sessionId)
|
|
92
|
+
args.push('resume');
|
|
93
|
+
args.push('--json');
|
|
94
|
+
if (o.codexModel)
|
|
95
|
+
args.push('-m', o.codexModel);
|
|
96
|
+
args.push('-c', `model_reasoning_effort="${o.thinkingEffort}"`);
|
|
97
|
+
if (o.codexFullAccess)
|
|
98
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
99
|
+
if (o.attachments?.length) {
|
|
100
|
+
for (const f of o.attachments)
|
|
101
|
+
args.push('--image', f);
|
|
102
|
+
}
|
|
103
|
+
if (o.codexExtraArgs?.length)
|
|
104
|
+
args.push(...o.codexExtraArgs);
|
|
105
|
+
if (o.sessionId)
|
|
106
|
+
args.push(o.sessionId);
|
|
107
|
+
args.push('-');
|
|
108
|
+
return args;
|
|
109
|
+
}
|
|
110
|
+
function codexParse(ev, s) {
|
|
111
|
+
const t = ev.type || '';
|
|
112
|
+
if (t === 'thread.started') {
|
|
113
|
+
s.sessionId = ev.thread_id ?? s.sessionId;
|
|
114
|
+
s.model = ev.model ?? s.model;
|
|
115
|
+
}
|
|
116
|
+
if (t === 'item.completed') {
|
|
117
|
+
const item = ev.item || {};
|
|
118
|
+
if (item.type === 'agent_message' && item.text?.trim()) {
|
|
119
|
+
s.msgs.push(item.text.trim());
|
|
120
|
+
s.text = s.msgs.join('\n\n');
|
|
121
|
+
}
|
|
122
|
+
if (item.type === 'reasoning' && (item.text || item.summary)?.trim()) {
|
|
123
|
+
s.thinkParts.push((item.text || item.summary).trim());
|
|
124
|
+
s.thinking = s.thinkParts.join('\n\n');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (t === 'turn.completed') {
|
|
128
|
+
const u = ev.usage;
|
|
129
|
+
if (u) {
|
|
130
|
+
s.inputTokens = u.input_tokens ?? s.inputTokens;
|
|
131
|
+
s.cachedInputTokens = u.cached_input_tokens ?? s.cachedInputTokens;
|
|
132
|
+
s.outputTokens = u.output_tokens ?? s.outputTokens;
|
|
133
|
+
}
|
|
134
|
+
s.model = ev.model ?? s.model;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function doCodexStream(opts) {
|
|
138
|
+
return run(codexCmd(opts), opts, codexParse);
|
|
139
|
+
}
|
|
140
|
+
// --- claude ---
|
|
141
|
+
function claudeCmd(o) {
|
|
142
|
+
const args = ['claude', '-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
|
|
143
|
+
if (o.claudeModel)
|
|
144
|
+
args.push('--model', o.claudeModel);
|
|
145
|
+
if (o.claudePermissionMode)
|
|
146
|
+
args.push('--permission-mode', o.claudePermissionMode);
|
|
147
|
+
if (o.sessionId)
|
|
148
|
+
args.push('--resume', o.sessionId);
|
|
149
|
+
if (o.attachments?.length) {
|
|
150
|
+
for (const f of o.attachments)
|
|
151
|
+
args.push('--input-file', f);
|
|
152
|
+
}
|
|
153
|
+
if (o.claudeExtraArgs?.length)
|
|
154
|
+
args.push(...o.claudeExtraArgs);
|
|
155
|
+
return args;
|
|
156
|
+
}
|
|
157
|
+
function claudeParse(ev, s) {
|
|
158
|
+
const t = ev.type || '';
|
|
159
|
+
if (t === 'system') {
|
|
160
|
+
s.sessionId = ev.session_id ?? s.sessionId;
|
|
161
|
+
s.model = ev.model ?? s.model;
|
|
162
|
+
s.thinkingEffort = ev.thinking_level ?? s.thinkingEffort;
|
|
163
|
+
}
|
|
164
|
+
if (t === 'stream_event') {
|
|
165
|
+
const inner = ev.event || {};
|
|
166
|
+
if (inner.type === 'content_block_delta') {
|
|
167
|
+
const d = inner.delta || {};
|
|
168
|
+
if (d.type === 'thinking_delta')
|
|
169
|
+
s.thinking += d.thinking || '';
|
|
170
|
+
else if (d.type === 'text_delta')
|
|
171
|
+
s.text += d.text || '';
|
|
172
|
+
}
|
|
173
|
+
if (inner.type === 'message_delta') {
|
|
174
|
+
const u = inner.usage;
|
|
175
|
+
if (u) {
|
|
176
|
+
s.inputTokens = u.input_tokens ?? s.inputTokens;
|
|
177
|
+
s.cachedInputTokens = u.cache_read_input_tokens ?? s.cachedInputTokens;
|
|
178
|
+
s.outputTokens = u.output_tokens ?? s.outputTokens;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
s.sessionId = ev.session_id ?? s.sessionId;
|
|
182
|
+
s.model = ev.model ?? s.model;
|
|
183
|
+
}
|
|
184
|
+
if (t === 'assistant') {
|
|
185
|
+
const contents = (ev.message || {}).content || [];
|
|
186
|
+
const th = contents.filter((b) => b?.type === 'thinking').map((b) => b.thinking || '').join('');
|
|
187
|
+
const tx = contents.filter((b) => b?.type === 'text').map((b) => b.text || '').join('');
|
|
188
|
+
if (th && !s.thinking.trim())
|
|
189
|
+
s.thinking = th;
|
|
190
|
+
if (tx && !s.text.trim())
|
|
191
|
+
s.text = tx;
|
|
192
|
+
}
|
|
193
|
+
if (t === 'result') {
|
|
194
|
+
s.sessionId = ev.session_id ?? s.sessionId;
|
|
195
|
+
s.model = ev.model ?? s.model;
|
|
196
|
+
if (ev.is_error && ev.errors?.length)
|
|
197
|
+
s.errors = ev.errors;
|
|
198
|
+
if (ev.result && !s.text.trim())
|
|
199
|
+
s.text = ev.result;
|
|
200
|
+
const u = ev.usage;
|
|
201
|
+
if (u) {
|
|
202
|
+
s.inputTokens = u.input_tokens ?? s.inputTokens;
|
|
203
|
+
s.cachedInputTokens = (u.cache_read_input_tokens ?? u.cached_input_tokens) ?? s.cachedInputTokens;
|
|
204
|
+
s.outputTokens = u.output_tokens ?? s.outputTokens;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export async function doClaudeStream(opts) {
|
|
209
|
+
const result = await run(claudeCmd(opts), opts, claudeParse);
|
|
210
|
+
// session not found → retry as new conversation
|
|
211
|
+
if (!result.ok && opts.sessionId && /no conversation found/i.test(result.message)) {
|
|
212
|
+
return run(claudeCmd({ ...opts, sessionId: null }), { ...opts, sessionId: null }, claudeParse);
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
// --- unified entry ---
|
|
217
|
+
export function doStream(opts) {
|
|
218
|
+
return opts.agent === 'codex' ? doCodexStream(opts) : doClaudeStream(opts);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Encode a workdir path to the Claude project directory name.
|
|
222
|
+
* Claude replaces `/` with `-` and strips the leading `-`.
|
|
223
|
+
*/
|
|
224
|
+
function claudeProjectDirName(workdir) {
|
|
225
|
+
return workdir.replace(/\//g, '-');
|
|
226
|
+
}
|
|
227
|
+
function readLines(filePath, maxLines) {
|
|
228
|
+
try {
|
|
229
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
230
|
+
const lines = [];
|
|
231
|
+
let start = 0;
|
|
232
|
+
for (let i = 0; i < maxLines; i++) {
|
|
233
|
+
const nl = content.indexOf('\n', start);
|
|
234
|
+
if (nl < 0) {
|
|
235
|
+
if (start < content.length)
|
|
236
|
+
lines.push(content.slice(start));
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
lines.push(content.slice(start, nl));
|
|
240
|
+
start = nl + 1;
|
|
241
|
+
}
|
|
242
|
+
return lines;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function parseClaudeSession(filePath, workdir) {
|
|
249
|
+
const lines = readLines(filePath, 10);
|
|
250
|
+
if (!lines.length)
|
|
251
|
+
return null;
|
|
252
|
+
try {
|
|
253
|
+
const sessionId = path.basename(filePath, '.jsonl');
|
|
254
|
+
const stat = fs.statSync(filePath);
|
|
255
|
+
let model = null;
|
|
256
|
+
let title = null;
|
|
257
|
+
for (const raw of lines) {
|
|
258
|
+
if (!raw || raw[0] !== '{')
|
|
259
|
+
continue;
|
|
260
|
+
try {
|
|
261
|
+
const ev = JSON.parse(raw);
|
|
262
|
+
if (ev.type === 'user' && !title) {
|
|
263
|
+
const content = ev.message?.content;
|
|
264
|
+
if (typeof content === 'string') {
|
|
265
|
+
title = content.slice(0, 120);
|
|
266
|
+
}
|
|
267
|
+
else if (Array.isArray(content)) {
|
|
268
|
+
const textBlock = content.find((b) => b?.type === 'text' && b.text && !b.text.startsWith('<'));
|
|
269
|
+
if (textBlock)
|
|
270
|
+
title = textBlock.text.slice(0, 120);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (ev.type === 'assistant' && !model) {
|
|
274
|
+
model = ev.message?.model ?? null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch { /* skip unparseable lines */ }
|
|
278
|
+
if (model && title)
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
sessionId,
|
|
283
|
+
agent: 'claude',
|
|
284
|
+
workdir,
|
|
285
|
+
model,
|
|
286
|
+
createdAt: stat.birthtime?.toISOString() ?? stat.mtime?.toISOString() ?? null,
|
|
287
|
+
title,
|
|
288
|
+
running: false,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function readLastLine(filePath) {
|
|
296
|
+
try {
|
|
297
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
298
|
+
const lines = content.trimEnd().split('\n');
|
|
299
|
+
return lines[lines.length - 1] || null;
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function parseCodexSession(filePath) {
|
|
306
|
+
const lines = readLines(filePath, 10);
|
|
307
|
+
const line = lines[0];
|
|
308
|
+
if (!line)
|
|
309
|
+
return null;
|
|
310
|
+
try {
|
|
311
|
+
const ev = JSON.parse(line);
|
|
312
|
+
if (ev.type !== 'session_meta')
|
|
313
|
+
return null;
|
|
314
|
+
const p = ev.payload || {};
|
|
315
|
+
let title = null;
|
|
316
|
+
for (const raw of lines.slice(1)) {
|
|
317
|
+
if (!raw || raw[0] !== '{')
|
|
318
|
+
continue;
|
|
319
|
+
try {
|
|
320
|
+
const item = JSON.parse(raw);
|
|
321
|
+
if (item.type === 'response_item' && item.payload?.role === 'user' && item.payload?.type === 'message') {
|
|
322
|
+
const content = item.payload.content;
|
|
323
|
+
if (Array.isArray(content)) {
|
|
324
|
+
const textBlock = content.find((b) => b?.type === 'input_text' && b.text && !/^[<#]/.test(b.text));
|
|
325
|
+
if (textBlock) {
|
|
326
|
+
title = textBlock.text.slice(0, 120);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch { /* skip */ }
|
|
333
|
+
}
|
|
334
|
+
// Codex writes task_complete as the last event when done
|
|
335
|
+
let running = false;
|
|
336
|
+
const last = readLastLine(filePath);
|
|
337
|
+
if (last) {
|
|
338
|
+
try {
|
|
339
|
+
const lastEv = JSON.parse(last);
|
|
340
|
+
running = !(lastEv.type === 'event_msg' && lastEv.payload?.type === 'task_complete');
|
|
341
|
+
}
|
|
342
|
+
catch { /* assume not running if unparseable */ }
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
sessionId: p.id ?? path.basename(filePath, '.jsonl'),
|
|
346
|
+
agent: 'codex',
|
|
347
|
+
workdir: p.cwd ?? null,
|
|
348
|
+
model: p.model_provider ?? null,
|
|
349
|
+
createdAt: p.timestamp ?? null,
|
|
350
|
+
title,
|
|
351
|
+
running,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** Collect session IDs from running `claude --resume <id>` processes. */
|
|
359
|
+
function getRunningClaudeSessionIds() {
|
|
360
|
+
try {
|
|
361
|
+
const out = execSync('ps -eo args 2>/dev/null', { encoding: 'utf-8', timeout: 3000 });
|
|
362
|
+
const ids = new Set();
|
|
363
|
+
for (const m of out.matchAll(/--resume\s+(\S+)/g))
|
|
364
|
+
ids.add(m[1]);
|
|
365
|
+
return ids;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return new Set();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function getClaudeSessions(opts) {
|
|
372
|
+
const limit = opts.limit ?? 50;
|
|
373
|
+
const home = process.env.HOME || '';
|
|
374
|
+
const projectDir = path.join(home, '.claude', 'projects', claudeProjectDirName(opts.workdir));
|
|
375
|
+
if (!fs.existsSync(projectDir)) {
|
|
376
|
+
return { ok: true, sessions: [], error: null };
|
|
377
|
+
}
|
|
378
|
+
const sessions = [];
|
|
379
|
+
try {
|
|
380
|
+
const files = fs.readdirSync(projectDir)
|
|
381
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
382
|
+
.map(f => ({ name: f, full: path.join(projectDir, f), mtime: fs.statSync(path.join(projectDir, f)).mtimeMs }))
|
|
383
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
384
|
+
.slice(0, limit);
|
|
385
|
+
const runningIds = getRunningClaudeSessionIds();
|
|
386
|
+
const now = Date.now();
|
|
387
|
+
for (const f of files) {
|
|
388
|
+
const info = parseClaudeSession(f.full, opts.workdir);
|
|
389
|
+
if (info) {
|
|
390
|
+
info.running = runningIds.has(info.sessionId) || (now - f.mtime < 10_000);
|
|
391
|
+
sessions.push(info);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
return { ok: false, sessions: [], error: e.message };
|
|
397
|
+
}
|
|
398
|
+
return { ok: true, sessions, error: null };
|
|
399
|
+
}
|
|
400
|
+
function getCodexSessions(opts) {
|
|
401
|
+
const limit = opts.limit ?? 50;
|
|
402
|
+
const home = process.env.HOME || '';
|
|
403
|
+
const sessionsRoot = path.join(home, '.codex', 'sessions');
|
|
404
|
+
if (!fs.existsSync(sessionsRoot)) {
|
|
405
|
+
return { ok: true, sessions: [], error: null };
|
|
406
|
+
}
|
|
407
|
+
const all = [];
|
|
408
|
+
try {
|
|
409
|
+
// Walk year/month/day directories
|
|
410
|
+
for (const year of fs.readdirSync(sessionsRoot)) {
|
|
411
|
+
const yp = path.join(sessionsRoot, year);
|
|
412
|
+
if (!fs.statSync(yp).isDirectory())
|
|
413
|
+
continue;
|
|
414
|
+
for (const month of fs.readdirSync(yp)) {
|
|
415
|
+
const mp = path.join(yp, month);
|
|
416
|
+
if (!fs.statSync(mp).isDirectory())
|
|
417
|
+
continue;
|
|
418
|
+
for (const day of fs.readdirSync(mp)) {
|
|
419
|
+
const dp = path.join(mp, day);
|
|
420
|
+
if (!fs.statSync(dp).isDirectory())
|
|
421
|
+
continue;
|
|
422
|
+
for (const f of fs.readdirSync(dp)) {
|
|
423
|
+
if (!f.endsWith('.jsonl'))
|
|
424
|
+
continue;
|
|
425
|
+
const full = path.join(dp, f);
|
|
426
|
+
all.push({ path: full, mtime: fs.statSync(full).mtimeMs });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch (e) {
|
|
433
|
+
return { ok: false, sessions: [], error: e.message };
|
|
434
|
+
}
|
|
435
|
+
// Sort newest first, parse and filter by workdir
|
|
436
|
+
all.sort((a, b) => b.mtime - a.mtime);
|
|
437
|
+
const sessions = [];
|
|
438
|
+
for (const entry of all) {
|
|
439
|
+
if (sessions.length >= limit)
|
|
440
|
+
break;
|
|
441
|
+
const info = parseCodexSession(entry.path);
|
|
442
|
+
if (info && info.workdir === opts.workdir)
|
|
443
|
+
sessions.push(info);
|
|
444
|
+
}
|
|
445
|
+
return { ok: true, sessions, error: null };
|
|
446
|
+
}
|
|
447
|
+
export function getSessions(opts) {
|
|
448
|
+
return opts.agent === 'codex' ? getCodexSessions(opts) : getClaudeSessions(opts);
|
|
449
|
+
}
|
|
450
|
+
function detectAgent(cmd, agent) {
|
|
451
|
+
let binPath = null;
|
|
452
|
+
try {
|
|
453
|
+
binPath = execSync(`which ${cmd} 2>/dev/null`, { encoding: 'utf-8' }).trim() || null;
|
|
454
|
+
}
|
|
455
|
+
catch { /* */ }
|
|
456
|
+
let version = null;
|
|
457
|
+
if (binPath) {
|
|
458
|
+
try {
|
|
459
|
+
version = execSync(`${cmd} --version 2>/dev/null`, { encoding: 'utf-8' }).trim().split('\n')[0] || null;
|
|
460
|
+
}
|
|
461
|
+
catch { /* */ }
|
|
462
|
+
}
|
|
463
|
+
return { agent, installed: !!binPath, path: binPath, version };
|
|
464
|
+
}
|
|
465
|
+
export function listAgents() {
|
|
466
|
+
return {
|
|
467
|
+
agents: [
|
|
468
|
+
detectAgent('claude', 'claude'),
|
|
469
|
+
detectAgent('codex', 'codex'),
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "The best IM-driven remote coding experience. Bridge AI coding agents to any IM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"codeclaw": "
|
|
7
|
+
"codeclaw": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"src/",
|
|
10
|
+
"dist/",
|
|
12
11
|
"LICENSE",
|
|
13
12
|
"README.md"
|
|
14
13
|
],
|
|
@@ -28,6 +27,8 @@
|
|
|
28
27
|
"url": "git+https://github.com/xiaotonng/codeclaw.git"
|
|
29
28
|
},
|
|
30
29
|
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"prepublishOnly": "npm run build",
|
|
31
32
|
"test": "vitest run",
|
|
32
33
|
"test:watch": "vitest"
|
|
33
34
|
},
|
|
@@ -35,6 +36,9 @@
|
|
|
35
36
|
"node": ">=18.0.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.3.5",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
38
42
|
"vitest": "^4.0.18"
|
|
39
43
|
}
|
|
40
44
|
}
|
package/bin/codeclaw.js
DELETED