cliclaw 1.0.21 → 1.0.22

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.22",
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
 
@@ -199,6 +237,15 @@ export async function askCodex(
199
237
  )
200
238
  }
201
239
 
240
+ /** Attach a per-session handler that is called whenever codex wants to run a command. */
241
+ export function setCodexApprovalHandler(
242
+ appSessionId: string,
243
+ handler: (callId: string, commandStr: string) => Promise<boolean>
244
+ ): void {
245
+ const ps = protoSessions.get(appSessionId)
246
+ if (ps) ps.approvalHandler = handler
247
+ }
248
+
202
249
  export function killCodexSession(appSessionId: string) {
203
250
  const ps = protoSessions.get(appSessionId)
204
251
  if (ps) {
@@ -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,10 +1,16 @@
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'
4
5
  import { askClaude, type TokenUsage } from '../agents/claude'
5
- import { askCodex } from '../agents/codex'
6
+ import { askCodex, setCodexApprovalHandler } 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,27 @@ export function registerMessageHandler(bot: Bot<Context>, storage: Storage, conf
96
102
 
97
103
  storage.addMessage(chatId, session.id, 'user', text)
98
104
 
105
+ // ── Codex approval handler ──────────────────────────────────────────────
106
+ if (session.model === 'codex') {
107
+ setCodexApprovalHandler(session.id, (callId, cmdStr) => {
108
+ const approvalId = randomUUID()
109
+ const label = `🔧 <b>Codex quer executar:</b>\n<code>${escapeHtml(cmdStr)}</code>`
110
+ ctx.api.sendMessage(chatId, label, {
111
+ ...replyOpts,
112
+ parse_mode: 'HTML',
113
+ reply_markup: {
114
+ 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) => {
121
+ registerApproval(approvalId, resolve)
122
+ })
123
+ })
124
+ }
125
+
99
126
  let response: string
100
127
  try {
101
128
  let result: { text: string; usage?: TokenUsage }