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/src/codeclaw.js DELETED
@@ -1,781 +0,0 @@
1
- /**
2
- * codeclaw — zero config, bridge AI coding agents to any IM.
3
- *
4
- * Core orchestrator: config, state management, engine execution, CLI entry point.
5
- * Channel-specific interaction is in separate files (channel-telegram.js, etc.).
6
- */
7
-
8
- import { spawn } from 'node:child_process';
9
- import fs from 'node:fs';
10
- import path from 'node:path';
11
- import { createInterface } from 'node:readline';
12
- import { execSync } from 'node:child_process';
13
- import { fileURLToPath } from 'node:url';
14
-
15
- export const VERSION = '0.1.0';
16
-
17
- // ---------------------------------------------------------------------------
18
- // Helpers
19
- // ---------------------------------------------------------------------------
20
-
21
- export function envBool(name, defaultVal) {
22
- const raw = process.env[name];
23
- if (raw === undefined || raw === null) return defaultVal;
24
- return ['1', 'true', 'yes', 'on'].includes(raw.trim().toLowerCase());
25
- }
26
-
27
- export function envInt(name, defaultVal) {
28
- const raw = process.env[name];
29
- if (raw === undefined || raw === null || raw.trim() === '') return defaultVal;
30
- const n = parseInt(raw, 10);
31
- return Number.isNaN(n) ? defaultVal : n;
32
- }
33
-
34
- export function parseAllowedChatIds(raw) {
35
- const ids = new Set();
36
- for (const token of raw.split(',')) {
37
- const t = token.trim();
38
- if (!t) continue;
39
- const n = parseInt(t, 10);
40
- if (!Number.isNaN(n)) ids.add(n);
41
- }
42
- return ids;
43
- }
44
-
45
- export function normalizeReasoningEffort(raw) {
46
- const value = raw.trim().toLowerCase();
47
- const allowed = new Set(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']);
48
- if (!allowed.has(value)) {
49
- throw new Error(
50
- 'Invalid CODEX_REASONING_EFFORT. Use one of: none, minimal, low, medium, high, xhigh'
51
- );
52
- }
53
- return value;
54
- }
55
-
56
- export function normalizeSessionName(raw) {
57
- const name = raw.trim().toLowerCase();
58
- if (!name) return 'default';
59
- if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(name)) {
60
- throw new Error(
61
- 'Invalid session name. Use 1-32 chars: a-z, 0-9, _ or -, start with letter/number.'
62
- );
63
- }
64
- return name;
65
- }
66
-
67
- export const VALID_ENGINES = new Set(['codex', 'claude']);
68
-
69
- export function normalizeEngine(raw) {
70
- const value = raw.trim().toLowerCase();
71
- if (!VALID_ENGINES.has(value)) {
72
- throw new Error(`Invalid engine. Use one of: ${[...VALID_ENGINES].sort().join(', ')}`);
73
- }
74
- return value;
75
- }
76
-
77
- function shellSplit(str) {
78
- const args = [];
79
- let current = '';
80
- let inSingle = false;
81
- let inDouble = false;
82
- for (let i = 0; i < str.length; i++) {
83
- const ch = str[i];
84
- if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
85
- if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
86
- if (ch === ' ' && !inSingle && !inDouble) {
87
- if (current) { args.push(current); current = ''; }
88
- continue;
89
- }
90
- current += ch;
91
- }
92
- if (current) args.push(current);
93
- return args;
94
- }
95
-
96
- function whichSync(cmd) {
97
- try {
98
- return execSync(`which ${cmd} 2>/dev/null`, { encoding: 'utf-8' }).trim() || null;
99
- } catch {
100
- return null;
101
- }
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // Core
106
- // ---------------------------------------------------------------------------
107
-
108
- export class CodeClaw {
109
- constructor() {
110
- const token = (process.env.TELEGRAM_BOT_TOKEN || process.env.CODECLAW_TOKEN || '').trim();
111
- if (!token) {
112
- throw new Error('Missing token. Use -t TOKEN or set CODECLAW_TOKEN / TELEGRAM_BOT_TOKEN');
113
- }
114
- this.token = token;
115
-
116
- const defaultWorkdir = process.cwd();
117
- this.workdir = path.resolve((process.env.CODECLAW_WORKDIR || defaultWorkdir).replace(/^~/, process.env.HOME || ''));
118
- this.stateDir = path.resolve((process.env.CODECLAW_STATE_DIR || '~/.codeclaw').replace(/^~/, process.env.HOME || ''));
119
-
120
- fs.mkdirSync(this.stateDir, { recursive: true });
121
- this.stateFile = path.join(this.stateDir, 'state.json');
122
- this.lockFile = path.join(this.stateDir, 'bridge.lock');
123
-
124
- this.pollTimeout = envInt('TELEGRAM_POLL_TIMEOUT', 45);
125
- this.runTimeout = envInt('CODECLAW_TIMEOUT', 300);
126
- this.requireMention = envBool('TELEGRAM_REQUIRE_MENTION_IN_GROUP', true);
127
- this.allowedChatIds = parseAllowedChatIds(
128
- process.env.TELEGRAM_ALLOWED_CHAT_IDS || process.env.CODECLAW_ALLOWED_IDS || ''
129
- );
130
-
131
- // Codex settings
132
- this.codexModel = (process.env.CODEX_MODEL || 'gpt-5.4').trim();
133
- this.codexReasoningEffort = normalizeReasoningEffort(
134
- process.env.CODEX_REASONING_EFFORT || 'xhigh'
135
- );
136
- this.codexFullAccess = envBool('CODEX_FULL_ACCESS', true);
137
- this.codexExtraArgs = shellSplit(process.env.CODEX_EXTRA_ARGS || '');
138
-
139
- // Claude settings
140
- this.claudeModel = (process.env.CLAUDE_MODEL || 'claude-opus-4-6').trim();
141
- this.claudePermissionMode = (process.env.CLAUDE_PERMISSION_MODE || 'bypassPermissions').trim();
142
- this.claudeExtraArgs = shellSplit(process.env.CLAUDE_EXTRA_ARGS || '');
143
-
144
- // Default engine
145
- this.defaultEngine = normalizeEngine(process.env.DEFAULT_ENGINE || 'claude');
146
-
147
- this.botUsername = '';
148
- this.botId = 0;
149
- this.running = true;
150
- this.lockFd = null;
151
- this._replacedOldProcess = false;
152
-
153
- this.state = { last_update_id: 0, chats: {} };
154
- this._resetState();
155
- }
156
-
157
- // -------------------------------------------------------------------
158
- // Logging
159
- // -------------------------------------------------------------------
160
-
161
- _log(msg, { err = false } = {}) {
162
- const ts = new Date().toTimeString().slice(0, 8);
163
- const out = err ? process.stderr : process.stdout;
164
- out.write(`[codeclaw ${ts}] ${msg}\n`);
165
- }
166
-
167
- // -------------------------------------------------------------------
168
- // State management
169
- // -------------------------------------------------------------------
170
-
171
- _resetState() {
172
- if (!fs.existsSync(this.stateFile)) return;
173
- try {
174
- const parsed = JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
175
- if (parsed && typeof parsed === 'object') {
176
- this.state.last_update_id = parseInt(parsed.last_update_id, 10) || 0;
177
- }
178
- } catch { /* ignore */ }
179
- this.state.chats = {};
180
- this._saveState();
181
- }
182
-
183
- _ensureChatState(chatId) {
184
- const key = String(chatId);
185
- if (!this.state.chats[key]) {
186
- this.state.chats[key] = {
187
- active: 'default',
188
- threads: { default: '' },
189
- engine: this.defaultEngine,
190
- };
191
- }
192
- const cs = this.state.chats[key];
193
- let active = normalizeSessionName(String(cs.active || 'default'));
194
- cs.active = active;
195
- if (!cs.engine) cs.engine = this.defaultEngine;
196
- const raw = cs.threads && typeof cs.threads === 'object' ? cs.threads : {};
197
- const norm = {};
198
- for (const [name, tid] of Object.entries(raw)) {
199
- norm[normalizeSessionName(String(name))] = String(tid).trim();
200
- }
201
- if (!norm[active]) norm[active] = '';
202
- cs.threads = norm;
203
- return cs;
204
- }
205
-
206
- _saveState() {
207
- fs.writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2), 'utf-8');
208
- }
209
-
210
- // -------------------------------------------------------------------
211
- // Session helpers
212
- // -------------------------------------------------------------------
213
-
214
- _sessionForChat(chatId) {
215
- const cs = this._ensureChatState(chatId);
216
- const name = cs.active;
217
- const tid = (cs.threads[name] || '').trim() || null;
218
- return [name, tid];
219
- }
220
-
221
- _engineForChat(chatId) {
222
- return this._ensureChatState(chatId).engine || this.defaultEngine;
223
- }
224
-
225
- _setEngineForChat(chatId, engine) {
226
- this._ensureChatState(chatId).engine = normalizeEngine(engine);
227
- this._saveState();
228
- }
229
-
230
- _setActiveSession(chatId, sessionName) {
231
- const cs = this._ensureChatState(chatId);
232
- const name = normalizeSessionName(sessionName);
233
- cs.active = name;
234
- if (!cs.threads[name]) cs.threads[name] = '';
235
- this._saveState();
236
- }
237
-
238
- _setSessionThread(chatId, sessionName, threadId) {
239
- const cs = this._ensureChatState(chatId);
240
- cs.threads[normalizeSessionName(sessionName)] = (threadId || '').trim();
241
- this._saveState();
242
- }
243
-
244
- _deleteSession(chatId, sessionName) {
245
- const cs = this._ensureChatState(chatId);
246
- const name = normalizeSessionName(sessionName);
247
- delete cs.threads[name];
248
- if (Object.keys(cs.threads).length === 0) cs.threads.default = '';
249
- if (cs.active === name) {
250
- cs.active = 'default';
251
- if (!cs.threads.default) cs.threads.default = '';
252
- }
253
- this._saveState();
254
- }
255
-
256
- // -------------------------------------------------------------------
257
- // Process lock & lifecycle
258
- // -------------------------------------------------------------------
259
-
260
- _readPidFile(filePath) {
261
- try {
262
- const content = fs.readFileSync(filePath, 'utf-8').trim();
263
- return content ? parseInt(content, 10) : null;
264
- } catch {
265
- return null;
266
- }
267
- }
268
-
269
- _killProcess(pid) {
270
- this._replacedOldProcess = true;
271
- this._log(`killing existing process (PID ${pid}) ...`);
272
- try {
273
- process.kill(pid, 'SIGTERM');
274
- const start = Date.now();
275
- while (Date.now() - start < 3000) {
276
- try { process.kill(pid, 0); } catch { break; }
277
- const wait = ms => { const end = Date.now() + ms; while (Date.now() < end) { /* busy wait */ } };
278
- wait(100);
279
- }
280
- try {
281
- process.kill(pid, 0);
282
- this._log(`force killing PID ${pid}`);
283
- process.kill(pid, 'SIGKILL');
284
- } catch { /* already dead */ }
285
- } catch { /* process doesn't exist */ }
286
- this._log(`old process (PID ${pid}) terminated`);
287
- }
288
-
289
- _acquireLock() {
290
- const oldPid = this._readPidFile(this.lockFile);
291
- try {
292
- this.lockFd = fs.openSync(this.lockFile, 'w');
293
- } catch (e) {
294
- throw new Error(`Failed to open lock file: ${this.lockFile}`);
295
- }
296
-
297
- // Try to write PID - if another process is running, kill it
298
- if (oldPid && oldPid !== process.pid) {
299
- try { process.kill(oldPid, 0); this._killProcess(oldPid); } catch { /* not running */ }
300
- }
301
-
302
- fs.writeSync(this.lockFd, String(process.pid));
303
- }
304
-
305
- _ensureSingleBot() {
306
- const pidFile = path.join(this.stateDir, `bot_${this.botId}.pid`);
307
- const oldPid = this._readPidFile(pidFile);
308
- if (oldPid && oldPid !== process.pid) {
309
- try {
310
- process.kill(oldPid, 0);
311
- this._log(`same bot @${this.botUsername} running elsewhere (PID ${oldPid})`);
312
- this._killProcess(oldPid);
313
- } catch { /* not running */ }
314
- }
315
- fs.writeFileSync(pidFile, String(process.pid), 'utf-8');
316
- }
317
-
318
- _handleSignal(sig) {
319
- this.running = false;
320
- this._log(`signal ${sig}, shutting down...`);
321
- this._stopKeepAlive();
322
- }
323
-
324
- // -------------------------------------------------------------------
325
- // Keep-alive (prevent idle sleep)
326
- // -------------------------------------------------------------------
327
-
328
- _startKeepAlive() {
329
- this._keepAliveProc = null;
330
- const platform = process.platform;
331
-
332
- if (platform === 'darwin') {
333
- const caffeinate = whichSync('caffeinate');
334
- if (caffeinate) {
335
- this._keepAliveProc = spawn('caffeinate', ['-dis'], {
336
- stdio: 'ignore',
337
- detached: true,
338
- });
339
- this._keepAliveProc.unref();
340
- this._log(`keep-alive: caffeinate started (PID ${this._keepAliveProc.pid})`);
341
- } else {
342
- this._log('keep-alive: caffeinate not found, skipping', { err: true });
343
- }
344
- } else if (platform === 'linux') {
345
- const inhibit = whichSync('systemd-inhibit');
346
- if (inhibit) {
347
- this._keepAliveProc = spawn('systemd-inhibit', [
348
- '--what=idle', '--who=codeclaw',
349
- '--why=AI coding agent running', 'sleep', 'infinity',
350
- ], { stdio: 'ignore', detached: true });
351
- this._keepAliveProc.unref();
352
- this._log(`keep-alive: systemd-inhibit started (PID ${this._keepAliveProc.pid})`);
353
- } else {
354
- this._log('keep-alive: systemd-inhibit not found, skipping', { err: true });
355
- }
356
- } else {
357
- this._log(`keep-alive: unsupported platform (${platform}), skipping`);
358
- }
359
- }
360
-
361
- _stopKeepAlive() {
362
- if (this._keepAliveProc) {
363
- try { this._keepAliveProc.kill('SIGTERM'); } catch { /* ignore */ }
364
- this._keepAliveProc = null;
365
- this._log('keep-alive: stopped');
366
- }
367
- }
368
-
369
- // -------------------------------------------------------------------
370
- // Engine command builders
371
- // -------------------------------------------------------------------
372
-
373
- _buildCodexCmd(threadId) {
374
- const common = ['--json'];
375
- if (this.codexModel) common.push('-m', this.codexModel);
376
- common.push('-c', `model_reasoning_effort="${this.codexReasoningEffort}"`);
377
- if (this.codexFullAccess) common.push('--dangerously-bypass-approvals-and-sandbox');
378
- common.push(...this.codexExtraArgs);
379
- if (threadId) {
380
- return ['codex', 'exec', 'resume', ...common, threadId, '-'];
381
- }
382
- return ['codex', 'exec', ...common, '-'];
383
- }
384
-
385
- _buildClaudeCmd(threadId) {
386
- const cmd = ['claude', '-p', '--verbose', '--output-format', 'stream-json', '--include-partial-messages'];
387
- if (this.claudeModel) cmd.push('--model', this.claudeModel);
388
- if (this.claudePermissionMode) cmd.push('--permission-mode', this.claudePermissionMode);
389
- if (threadId) cmd.push('--resume', threadId);
390
- cmd.push(...this.claudeExtraArgs);
391
- return cmd;
392
- }
393
-
394
- // -------------------------------------------------------------------
395
- // Engine execution
396
- // -------------------------------------------------------------------
397
-
398
- spawnEngine(prompt, engine, threadId) {
399
- const cmd = engine === 'codex'
400
- ? this._buildCodexCmd(threadId)
401
- : this._buildClaudeCmd(threadId);
402
- const resume = threadId ? ` resume=${threadId.slice(0, 12)}` : ' new-thread';
403
- this._log(`spawn ${engine}${resume} prompt=${JSON.stringify(prompt.slice(0, 80))}`);
404
- this._log(` cmd: ${cmd.join(' ').slice(0, 200)}`);
405
-
406
- const proc = spawn(cmd[0], cmd.slice(1), {
407
- cwd: this.workdir,
408
- stdio: ['pipe', 'pipe', 'pipe'],
409
- });
410
-
411
- if (proc.stdin) {
412
- try {
413
- proc.stdin.write(prompt);
414
- proc.stdin.end();
415
- } catch { /* broken pipe */ }
416
- }
417
-
418
- return proc;
419
- }
420
-
421
- parseEvents(proc, engine, threadId, onText) {
422
- return new Promise((resolve) => {
423
- const start = Date.now();
424
- let collectedText = '';
425
- let discoveredThread = threadId;
426
- let usageInput = null;
427
- let usageCached = null;
428
- let usageOutput = null;
429
- const messagesBuffer = [];
430
- let stderr = '';
431
- const deadline = Date.now() + this.runTimeout * 1000;
432
-
433
- if (proc.stderr) {
434
- proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
435
- }
436
-
437
- const rl = createInterface({ input: proc.stdout, crlfDelay: Infinity });
438
-
439
- rl.on('line', (rawLine) => {
440
- if (Date.now() > deadline) {
441
- proc.kill('SIGKILL');
442
- return;
443
- }
444
- const line = rawLine.trim();
445
- if (!line || !line.startsWith('{')) return;
446
-
447
- let event;
448
- try { event = JSON.parse(line); } catch { return; }
449
-
450
- const evType = event.type || '';
451
-
452
- // --- Codex events ---
453
- if (evType === 'thread.started') {
454
- discoveredThread = event.thread_id || discoveredThread;
455
- }
456
- if (evType === 'item.completed') {
457
- const item = event.item || {};
458
- if (item.type === 'agent_message') {
459
- const text = (item.text || '').trim();
460
- if (text) {
461
- messagesBuffer.push(text);
462
- collectedText = messagesBuffer.join('\n\n');
463
- onText(collectedText);
464
- }
465
- }
466
- }
467
- if (evType === 'turn.completed') {
468
- const usage = event.usage;
469
- if (usage && typeof usage === 'object') {
470
- if (typeof usage.input_tokens === 'number') usageInput = usage.input_tokens;
471
- if (typeof usage.cached_input_tokens === 'number') usageCached = usage.cached_input_tokens;
472
- if (typeof usage.output_tokens === 'number') usageOutput = usage.output_tokens;
473
- }
474
- }
475
-
476
- // --- Claude Code events ---
477
- if (evType === 'system') {
478
- if (event.session_id) discoveredThread = event.session_id;
479
- }
480
-
481
- if (evType === 'stream_event') {
482
- const inner = event.event || {};
483
- const innerType = inner.type || '';
484
- if (innerType === 'content_block_delta') {
485
- const delta = inner.delta || {};
486
- if (delta.type === 'text_delta') {
487
- collectedText += delta.text || '';
488
- onText(collectedText);
489
- }
490
- } else if (innerType === 'message_delta') {
491
- const su = inner.usage;
492
- if (su && typeof su === 'object') {
493
- if (typeof su.input_tokens === 'number') usageInput = su.input_tokens;
494
- if (typeof su.cache_read_input_tokens === 'number') usageCached = su.cache_read_input_tokens;
495
- if (typeof su.output_tokens === 'number') usageOutput = su.output_tokens;
496
- }
497
- }
498
- if (event.session_id) discoveredThread = event.session_id;
499
- }
500
-
501
- if (evType === 'assistant') {
502
- const contents = (event.message || {}).content || [];
503
- const fullText = contents
504
- .filter(b => b && b.type === 'text')
505
- .map(b => b.text || '')
506
- .join('');
507
- if (fullText && !collectedText.trim()) {
508
- collectedText = fullText;
509
- onText(collectedText);
510
- }
511
- }
512
-
513
- if (evType === 'result') {
514
- if (event.session_id) discoveredThread = event.session_id;
515
- const resultText = event.result || '';
516
- if (resultText && !collectedText.trim()) collectedText = resultText;
517
- const usageData = event.usage;
518
- if (usageData && typeof usageData === 'object') {
519
- if (typeof usageData.input_tokens === 'number') usageInput = usageData.input_tokens;
520
- const ca = usageData.cache_read_input_tokens ?? usageData.cached_input_tokens;
521
- if (typeof ca === 'number') usageCached = ca;
522
- if (typeof usageData.output_tokens === 'number') usageOutput = usageData.output_tokens;
523
- }
524
- }
525
- });
526
-
527
- proc.on('close', (code) => {
528
- const elapsed = (Date.now() - start) / 1000;
529
- const ok = code === 0;
530
-
531
- if (!collectedText.trim() && messagesBuffer.length) {
532
- collectedText = messagesBuffer.join('\n\n');
533
- }
534
-
535
- let message;
536
- if (collectedText.trim()) {
537
- message = collectedText.trim();
538
- } else if (ok) {
539
- message = '(no textual response)';
540
- } else {
541
- message = `Failed (exit=${code}).\n\n${stderr.trim() || '(no output)'}`;
542
- }
543
-
544
- resolve({
545
- threadId: discoveredThread,
546
- message,
547
- ok,
548
- elapsedS: elapsed,
549
- inputTokens: usageInput,
550
- cachedInputTokens: usageCached,
551
- outputTokens: usageOutput,
552
- });
553
- });
554
-
555
- proc.on('error', (err) => {
556
- this._log(`stream error: ${err}`, { err: true });
557
- const elapsed = (Date.now() - start) / 1000;
558
- resolve({
559
- threadId: discoveredThread,
560
- message: `Failed: ${err.message}`,
561
- ok: false,
562
- elapsedS: elapsed,
563
- inputTokens: usageInput,
564
- cachedInputTokens: usageCached,
565
- outputTokens: usageOutput,
566
- });
567
- });
568
- });
569
- }
570
-
571
- // -------------------------------------------------------------------
572
- // Preflight & run
573
- // -------------------------------------------------------------------
574
-
575
- async preflight() {
576
- const url = `https://api.telegram.org/bot${this.token}/getMe`;
577
- this._log('preflight: validating bot token...');
578
-
579
- const resp = await fetch(url, {
580
- method: 'POST',
581
- headers: { 'Content-Type': 'application/json' },
582
- body: '{}',
583
- });
584
- const data = await resp.json();
585
- const me = data.result || {};
586
- this.botUsername = me.username || '';
587
- this.botId = parseInt(me.id, 10) || 0;
588
- this._log(`bot: @${this.botUsername} (id=${this.botId})`);
589
-
590
- if (!fs.existsSync(this.workdir)) {
591
- throw new Error(`Workdir not found: ${this.workdir}`);
592
- }
593
-
594
- for (const eng of [...VALID_ENGINES].sort()) {
595
- const p = whichSync(eng);
596
- this._log(`engine ${eng}: ${p || 'NOT FOUND'}`);
597
- }
598
-
599
- this._log(`config: default_engine=${this.defaultEngine} workdir=${this.workdir}`);
600
- this._log(`config: timeout=${this.runTimeout}s full_access=${this.codexFullAccess} claude_mode=${this.claudePermissionMode}`);
601
- if (this.allowedChatIds.size) {
602
- this._log(`config: allowed_ids=${[...this.allowedChatIds].sort()}`);
603
- } else {
604
- this._log('config: allowed_ids=ANY (no restriction)');
605
- }
606
- }
607
-
608
- async run() {
609
- this._acquireLock();
610
- process.on('SIGINT', () => this._handleSignal('SIGINT'));
611
- process.on('SIGTERM', () => this._handleSignal('SIGTERM'));
612
- await this.preflight();
613
- this._ensureSingleBot();
614
- this._startKeepAlive();
615
-
616
- try {
617
- const { TelegramChannel } = await import('./channel-telegram.js');
618
- const channel = new TelegramChannel(this);
619
- await channel.run();
620
- } finally {
621
- this._stopKeepAlive();
622
- }
623
- }
624
- }
625
-
626
- // ---------------------------------------------------------------------------
627
- // CLI
628
- // ---------------------------------------------------------------------------
629
-
630
- const SUPPORTED_CHANNELS = new Set(['telegram']);
631
- const PLANNED_CHANNELS = new Set(['slack', 'discord', 'dingtalk', 'feishu']);
632
- const ALL_CHANNELS = new Set([...SUPPORTED_CHANNELS, ...PLANNED_CHANNELS]);
633
-
634
- function parseArgs(argv) {
635
- const args = {
636
- channel: process.env.CODECLAW_CHANNEL || 'telegram',
637
- token: null,
638
- engine: null,
639
- model: null,
640
- workdir: null,
641
- fullAccess: null,
642
- safeMode: false,
643
- allowedIds: null,
644
- timeout: null,
645
- selfCheck: false,
646
- version: false,
647
- help: false,
648
- };
649
-
650
- const it = argv[Symbol.iterator]();
651
- for (const arg of it) {
652
- switch (arg) {
653
- case '-c': case '--channel': args.channel = it.next().value; break;
654
- case '-t': case '--token': args.token = it.next().value; break;
655
- case '-e': case '--engine': args.engine = it.next().value; break;
656
- case '-m': case '--model': args.model = it.next().value; break;
657
- case '-w': case '--workdir': args.workdir = it.next().value; break;
658
- case '--full-access': args.fullAccess = true; break;
659
- case '--safe-mode': args.safeMode = true; break;
660
- case '--allowed-ids': args.allowedIds = it.next().value; break;
661
- case '--timeout': args.timeout = parseInt(it.next().value, 10); break;
662
- case '--self-check': args.selfCheck = true; break;
663
- case '-v': case '--version': args.version = true; break;
664
- case '-h': case '--help': args.help = true; break;
665
- default:
666
- if (arg.startsWith('-')) {
667
- process.stderr.write(`Unknown option: ${arg}\n`);
668
- process.exit(1);
669
- }
670
- }
671
- }
672
- return args;
673
- }
674
-
675
- function printHelp() {
676
- process.stdout.write(`codeclaw — bridge AI coding agents to your IM.
677
-
678
- Usage: codeclaw [options]
679
- npx codeclaw [options]
680
-
681
- Connection:
682
- -c, --channel <ch> IM channel (default: telegram)
683
- -t, --token <token> Bot token
684
-
685
- Engine:
686
- -e, --engine <eng> AI engine: claude or codex (default: claude)
687
- -m, --model <model> Model override
688
- -w, --workdir <dir> Working directory (default: cwd)
689
-
690
- Access control:
691
- --full-access Agent runs without confirmation (default)
692
- --safe-mode Require confirmation for destructive ops
693
- --allowed-ids <ids> Comma-separated user/chat ID whitelist
694
- --timeout <secs> Max seconds per request (default: 300)
695
-
696
- Other:
697
- --self-check Validate setup and exit
698
- -v, --version Show version
699
- -h, --help Show this help
700
-
701
- Environment variables:
702
- CODECLAW_TOKEN Bot token (same as -t)
703
- CODECLAW_WORKDIR Working directory (same as -w)
704
- CODECLAW_TIMEOUT Timeout in seconds (same as --timeout)
705
- DEFAULT_ENGINE AI engine (same as -e)
706
- CLAUDE_MODEL Claude model name
707
- CLAUDE_PERMISSION_MODE bypassPermissions (default) or default
708
- CLAUDE_EXTRA_ARGS Extra args passed to claude CLI
709
- CODEX_MODEL Codex model name
710
- CODEX_REASONING_EFFORT none | minimal | low | medium | high | xhigh
711
- CODEX_EXTRA_ARGS Extra args passed to codex CLI
712
-
713
- Examples:
714
- codeclaw -t $BOT_TOKEN
715
- codeclaw -t $BOT_TOKEN -e codex --safe-mode --allowed-ids 123456,789012
716
- codeclaw -t $BOT_TOKEN -m sonnet -w ~/projects/my-app
717
- codeclaw -t $BOT_TOKEN --self-check
718
- `);
719
- }
720
-
721
- export async function main() {
722
- const argv = process.argv.slice(2);
723
- const args = parseArgs(argv);
724
-
725
- if (args.version) {
726
- process.stdout.write(`codeclaw ${VERSION}\n`);
727
- return 0;
728
- }
729
-
730
- if (args.help) {
731
- printHelp();
732
- return 0;
733
- }
734
-
735
- if (args.channel && !ALL_CHANNELS.has(args.channel)) {
736
- process.stderr.write(`Unknown channel: ${args.channel}\n`);
737
- return 1;
738
- }
739
-
740
- if (PLANNED_CHANNELS.has(args.channel)) {
741
- process.stderr.write(
742
- `[codeclaw] '${args.channel}' is planned but not yet implemented. Currently supported: ${[...SUPPORTED_CHANNELS].sort().join(', ')}\n`
743
- );
744
- return 1;
745
- }
746
-
747
- // Map CLI flags to env vars
748
- const token = args.token || process.env.CODECLAW_TOKEN || '';
749
- if (token) process.env.TELEGRAM_BOT_TOKEN = token;
750
- if (args.engine) process.env.DEFAULT_ENGINE = args.engine;
751
- if (args.workdir) process.env.CODECLAW_WORKDIR = args.workdir;
752
- if (args.model) {
753
- const engine = args.engine || process.env.DEFAULT_ENGINE || 'claude';
754
- if (engine === 'codex') {
755
- process.env.CODEX_MODEL = args.model;
756
- } else {
757
- process.env.CLAUDE_MODEL = args.model;
758
- }
759
- }
760
- if (args.allowedIds) process.env.TELEGRAM_ALLOWED_CHAT_IDS = args.allowedIds;
761
- if (args.timeout != null) process.env.CODECLAW_TIMEOUT = String(args.timeout);
762
-
763
- if (args.safeMode) {
764
- process.env.CODEX_FULL_ACCESS = 'false';
765
- process.env.CLAUDE_PERMISSION_MODE = 'default';
766
- } else if (args.fullAccess || envBool('CODECLAW_FULL_ACCESS', true)) {
767
- process.env.CODEX_FULL_ACCESS = 'true';
768
- process.env.CLAUDE_PERMISSION_MODE = 'bypassPermissions';
769
- }
770
-
771
- const claw = new CodeClaw();
772
- if (args.selfCheck) {
773
- claw._acquireLock();
774
- await claw.preflight();
775
- process.stdout.write('[codeclaw] ok\n');
776
- return 0;
777
- }
778
-
779
- await claw.run();
780
- return 0;
781
- }