cliclaw 1.0.21 → 1.0.23

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/index.ts CHANGED
@@ -3,6 +3,7 @@ import { loadConfig } from './src/config'
3
3
  import { Storage } from './src/storage'
4
4
  import { registerCommands } from './src/handlers/commands'
5
5
  import { registerMessageHandler } from './src/handlers/messages'
6
+ import { respondApproval } from './src/handlers/approvals'
6
7
 
7
8
  const config = loadConfig()
8
9
  const storage = new Storage(config.DATA_DIR)
@@ -11,6 +12,26 @@ const bot = new Bot(config.TELEGRAM_BOT_TOKEN)
11
12
  registerCommands(bot, storage, config)
12
13
  registerMessageHandler(bot, storage, config)
13
14
 
15
+ // ── Codex exec approval callbacks ──────────────────────────────────────────
16
+ bot.on('callback_query:data', async (ctx) => {
17
+ const data = ctx.callbackQuery.data
18
+ if (!data.startsWith('capprove:') && !data.startsWith('cdeny:')) return
19
+ const [prefix, approvalId] = data.split(':')
20
+ const approved = prefix === 'capprove'
21
+ const found = respondApproval(approvalId, approved)
22
+ const label = approved ? '✅ Aprovado' : '❌ Negado'
23
+ try {
24
+ if (found) {
25
+ await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } })
26
+ await ctx.editMessageText(
27
+ (ctx.callbackQuery.message?.text ?? '') + `\n\n${label}`,
28
+ { parse_mode: 'HTML' }
29
+ )
30
+ }
31
+ await ctx.answerCallbackQuery({ text: label })
32
+ } catch {}
33
+ })
34
+
14
35
  bot.catch((err) => {
15
36
  const ctx = err.ctx
16
37
  console.error(`[OpenClaw] Erro em update ${ctx.update.update_id}: ${err.error}`)
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "cliclaw",
3
- "version": "1.0.21",
4
- "description": "Telegram bot bridging AI CLIs (Claude Code, Codex) to Forum Topics",
5
- "main": "index.ts",
6
- "scripts": {
7
- "start": "tsx index.ts",
8
- "dev": "tsx watch index.ts",
9
- "setup": "node bin/cli.js setup"
10
- },
11
- "dependencies": {
12
- "@anthropic-ai/sdk": "^0.39.0",
13
- "grammy": "^1.31.3",
14
- "openai": "^4.97.0",
15
- "tsx": "^4.21.0"
16
- },
17
- "devDependencies": {
18
- "@types/node": "^20.0.0",
19
- "typescript": "^5.0.0"
20
- },
21
- "repository": {
22
- "type": "git",
23
- "url": "https://github.com/luizfeer/cliclaw.git"
24
- },
25
- "keywords": [
26
- "telegram",
27
- "claude",
28
- "codex",
29
- "cli",
30
- "bot",
31
- "forum"
32
- ],
33
- "license": "MIT",
34
- "bin": {
35
- "cliclaw": "./bin/cli.js"
36
- },
37
- "files": [
38
- "bin/",
39
- "src/",
40
- "index.ts",
41
- "ecosystem.config.js",
42
- ".env.example",
43
- "tsconfig.json"
44
- ],
45
- "engines": {
46
- "node": ">=18"
47
- }
48
- }
1
+ {
2
+ "name": "cliclaw",
3
+ "version": "1.0.23",
4
+ "description": "Telegram bot bridging AI CLIs (Claude Code, Codex) to Forum Topics",
5
+ "main": "index.ts",
6
+ "scripts": {
7
+ "start": "tsx index.ts",
8
+ "dev": "tsx watch index.ts",
9
+ "setup": "node bin/cli.js setup"
10
+ },
11
+ "dependencies": {
12
+ "@anthropic-ai/sdk": "^0.39.0",
13
+ "grammy": "^1.31.3",
14
+ "openai": "^4.97.0",
15
+ "tsx": "^4.21.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.0.0",
19
+ "typescript": "^5.0.0"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/luizfeer/cliclaw.git"
24
+ },
25
+ "keywords": [
26
+ "telegram",
27
+ "claude",
28
+ "codex",
29
+ "cli",
30
+ "bot",
31
+ "forum"
32
+ ],
33
+ "license": "MIT",
34
+ "bin": {
35
+ "cliclaw": "./bin/cli.js"
36
+ },
37
+ "files": [
38
+ "bin/",
39
+ "src/",
40
+ "index.ts",
41
+ "ecosystem.config.js",
42
+ ".env.example",
43
+ "tsconfig.json"
44
+ ],
45
+ "engines": {
46
+ "node": ">=18"
47
+ }
48
+ }
@@ -26,16 +26,25 @@ interface PendingReq {
26
26
  }
27
27
 
28
28
  interface ProtoSession {
29
- proc: ChildProcess
30
- sessionId: string
31
- lastUsed: number
32
- pending: Map<string, PendingReq>
33
- buf: string // incomplete stdout line buffer
29
+ proc: ChildProcess
30
+ sessionId: string
31
+ lastUsed: number
32
+ pending: Map<string, PendingReq>
33
+ buf: string // incomplete stdout line buffer
34
+ approvalHandler?: (callId: string, commandStr: string) => Promise<boolean>
34
35
  }
35
36
 
36
37
  // Map from cliclaw session.id → ProtoSession
37
38
  const protoSessions = new Map<string, ProtoSession>()
38
39
 
40
+ function formatApprovalCommand(cmd: any): string {
41
+ if (!cmd) return '(unknown)'
42
+ if (cmd.type === 'shell' && Array.isArray(cmd.command)) return cmd.command.join(' ')
43
+ if (typeof cmd === 'string') return cmd
44
+ if (Array.isArray(cmd)) return cmd.join(' ')
45
+ return JSON.stringify(cmd)
46
+ }
47
+
39
48
  // ─── cleanup idle sessions every 5 min ───────────────────────────────────────
40
49
  setInterval(() => {
41
50
  const now = Date.now()
@@ -112,6 +121,35 @@ function handleProtoEvent(ps: ProtoSession, obj: any) {
112
121
 
113
122
  if (msg?.type === 'session_configured') {
114
123
  ps.sessionId = msg.session_id ?? ''
124
+ // Ask codex to route exec approvals to us instead of auto-approving
125
+ try {
126
+ ps.proc.stdin!.write(JSON.stringify({
127
+ id: 'cfg-approval',
128
+ op: { type: 'configure', config: { approval_policy: 'user_approval' } },
129
+ }) + '\n')
130
+ } catch {}
131
+ return
132
+ }
133
+
134
+ // ── exec approval request ───────────────────────────────────────────────────
135
+ if (msg?.type === 'exec_approval_request') {
136
+ const callId = msg.call_id ?? msgId
137
+ const commandStr = formatApprovalCommand(msg.command ?? msg.cmd)
138
+ const respond = (approved: boolean) => {
139
+ try {
140
+ ps.proc.stdin!.write(JSON.stringify({
141
+ id: randomUUID(),
142
+ op: { type: 'exec_approval_response', call_id: callId, approved },
143
+ }) + '\n')
144
+ } catch {}
145
+ }
146
+ if (ps.approvalHandler) {
147
+ ps.approvalHandler(callId, commandStr)
148
+ .then(respond)
149
+ .catch(() => respond(false))
150
+ } else {
151
+ respond(true) // no handler → auto-approve
152
+ }
115
153
  return
116
154
  }
117
155
 
@@ -161,11 +199,14 @@ function getOrStart(appSessionId: string): ProtoSession {
161
199
 
162
200
  // ─── public API ──────────────────────────────────────────────────────────────
163
201
  export async function askCodex(
164
- session: { id: string; codexThreadId?: string },
165
- userMessage: string,
166
- _onNewThreadId?: (id: string) => void
202
+ session: { id: string; codexThreadId?: string },
203
+ userMessage: string,
204
+ _onNewThreadId?: (id: string) => void,
205
+ approvalHandler?: (callId: string, commandStr: string) => Promise<boolean>
167
206
  ): Promise<{ text: string; usage?: TokenUsage }> {
168
207
  const ps = getOrStart(session.id)
208
+ // Update handler every call (chat context may have changed)
209
+ if (approvalHandler !== undefined) ps.approvalHandler = approvalHandler
169
210
  const msgId = randomUUID()
170
211
 
171
212
  return new Promise((resolve, reject) => {
package/src/config.ts CHANGED
@@ -5,6 +5,7 @@ import { homedir } from 'os'
5
5
 
6
6
  export type AgentName = 'claude' | 'codex'
7
7
  export type PermissionMode = 'auto' | 'session' | 'ask'
8
+ export type CodexApproval = 'ask' | 'allow'
8
9
 
9
10
  export interface Config {
10
11
  TELEGRAM_BOT_TOKEN: string
@@ -12,6 +13,7 @@ export interface Config {
12
13
  FORUM_GROUP_ID: string
13
14
  TELEGRAM_ADMIN_IDS: string[]
14
15
  PERMISSION_MODE: PermissionMode
16
+ CODEX_APPROVAL: CodexApproval
15
17
  availableAgents: AgentName[]
16
18
  }
17
19
 
@@ -59,6 +61,9 @@ export function loadConfig(): Config {
59
61
  rawMode === 'session' ? 'session' :
60
62
  rawMode === 'ask' ? 'ask' : 'auto'
61
63
 
64
+ const rawApproval = (process.env.CODEX_APPROVAL || 'ask').toLowerCase()
65
+ const codexApproval: CodexApproval = rawApproval === 'allow' ? 'allow' : 'ask'
66
+
62
67
  const config: Config = {
63
68
  TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN || '',
64
69
  DATA_DIR: process.env.DATA_DIR || join(USER_DIR, 'data'),
@@ -66,6 +71,7 @@ export function loadConfig(): Config {
66
71
  TELEGRAM_ADMIN_IDS: (process.env.TELEGRAM_ADMIN_IDS || '')
67
72
  .split(',').map(id => id.trim()).filter(Boolean),
68
73
  PERMISSION_MODE: permMode,
74
+ CODEX_APPROVAL: codexApproval,
69
75
  availableAgents: checkAgents(),
70
76
  }
71
77
 
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared state for pending Codex exec approval requests.
3
+ * When codex wants to run a shell command, the bot sends a Telegram message
4
+ * with Approve/Deny buttons. The user's answer is routed back here.
5
+ */
6
+
7
+ interface PendingApproval {
8
+ resolve: (approved: boolean) => void
9
+ timeout: ReturnType<typeof setTimeout>
10
+ }
11
+
12
+ const pending = new Map<string, PendingApproval>()
13
+
14
+ /** Store a pending approval that will resolve when the user responds. */
15
+ export function registerApproval(
16
+ id: string,
17
+ resolve: (approved: boolean) => void,
18
+ ttlMs = 5 * 60 * 1000 // auto-deny after 5 min
19
+ ): void {
20
+ const timeout = setTimeout(() => respondApproval(id, false), ttlMs)
21
+ pending.set(id, { resolve, timeout })
22
+ }
23
+
24
+ /** Called by the callback-query handler when the user clicks Approve/Deny. */
25
+ export function respondApproval(id: string, approved: boolean): boolean {
26
+ const p = pending.get(id)
27
+ if (!p) return false
28
+ clearTimeout(p.timeout)
29
+ pending.delete(id)
30
+ p.resolve(approved)
31
+ return true
32
+ }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from 'crypto'
1
2
  import type { Bot, Context, Api } from 'grammy'
2
3
  import type { Storage, Session } from '../storage'
3
4
  import type { Config } from '../config'
@@ -5,6 +6,11 @@ import { askClaude, type TokenUsage } from '../agents/claude'
5
6
  import { askCodex } from '../agents/codex'
6
7
  import { mdToTg, splitHtml } from '../utils/markdown'
7
8
  import { getLang, t } from '../i18n'
9
+ import { registerApproval, respondApproval } from './approvals'
10
+
11
+ function escapeHtml(s: string): string {
12
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
13
+ }
8
14
 
9
15
  function formatUsage(usage: TokenUsage): string {
10
16
  const fmt = (n: number) => n.toLocaleString('en')
@@ -96,6 +102,25 @@ export function registerMessageHandler(bot: Bot<Context>, storage: Storage, conf
96
102
 
97
103
  storage.addMessage(chatId, session.id, 'user', text)
98
104
 
105
+ // ── build codex approval handler (only if CODEX_APPROVAL=ask) ──────────
106
+ const codexApprovalHandler = (session.model === 'codex' && config.CODEX_APPROVAL === 'ask')
107
+ ? (callId: string, cmdStr: string) => {
108
+ const approvalId = randomUUID()
109
+ ctx.api.sendMessage(chatId,
110
+ `🔧 <b>Codex quer executar:</b>\n<code>${escapeHtml(cmdStr)}</code>`,
111
+ {
112
+ ...replyOpts,
113
+ parse_mode: 'HTML',
114
+ reply_markup: { inline_keyboard: [[
115
+ { text: '✅ Aprovar', callback_data: `capprove:${approvalId}` },
116
+ { text: '❌ Negar', callback_data: `cdeny:${approvalId}` },
117
+ ]] },
118
+ }
119
+ ).catch(() => {})
120
+ return new Promise<boolean>((resolve) => registerApproval(approvalId, resolve))
121
+ }
122
+ : undefined
123
+
99
124
  let response: string
100
125
  try {
101
126
  let result: { text: string; usage?: TokenUsage }
@@ -104,7 +129,8 @@ export function registerMessageHandler(bot: Bot<Context>, storage: Storage, conf
104
129
  (id) => storage.setClaudeSessionId(chatId, session!.id, id))
105
130
  } else {
106
131
  result = await askCodex(session, text,
107
- (id) => storage.setCodexThreadId(chatId, session!.id, id))
132
+ (id) => storage.setCodexThreadId(chatId, session!.id, id),
133
+ codexApprovalHandler)
108
134
  }
109
135
  response = (isFirstMessage && result.usage) ? result.text + formatUsage(result.usage) : result.text
110
136
  } catch (err: any) {