cliclaw 1.0.26 → 1.0.34

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
@@ -9,27 +9,59 @@ const config = loadConfig()
9
9
  const storage = new Storage(config.DATA_DIR)
10
10
  const bot = new Bot(config.TELEGRAM_BOT_TOKEN)
11
11
 
12
+ // ── debug: log every incoming update type ──────────────────────────────────
13
+ bot.use(async (ctx, next) => {
14
+ const type = ctx.updateType ?? 'unknown'
15
+ const extra = type === 'callback_query'
16
+ ? ` data="${(ctx.callbackQuery?.data ?? '').slice(0, 40)}"`
17
+ : type === 'message'
18
+ ? ` text="${(ctx.message?.text ?? '').slice(0, 30)}"`
19
+ : ''
20
+ console.log(`[update] ${type}${extra}`)
21
+ return next()
22
+ })
23
+
12
24
  registerCommands(bot, storage, config)
13
25
  registerMessageHandler(bot, storage, config)
14
26
 
15
27
  // ── Codex exec approval callbacks ──────────────────────────────────────────
16
28
  bot.on('callback_query:data', async (ctx) => {
17
29
  const data = ctx.callbackQuery.data
30
+ console.log(`[callback] recebido: ${data.slice(0, 60)}`)
31
+
18
32
  if (!data.startsWith('capprove:') && !data.startsWith('cdeny:')) return
19
- const [prefix, approvalId] = data.split(':')
20
- const approved = prefix === 'capprove'
33
+
34
+ const colonIdx = data.indexOf(':')
35
+ const prefix = data.slice(0, colonIdx)
36
+ const approvalId = data.slice(colonIdx + 1)
37
+ const approved = prefix === 'capprove'
38
+
39
+ console.log(`[callback] clicou ${approved ? 'APROVAR' : 'NEGAR'} id=${approvalId.slice(0, 8)}...`)
40
+
21
41
  const found = respondApproval(approvalId, approved)
42
+ console.log(`[callback] respondApproval found=${found}`)
43
+
22
44
  const label = approved ? '✅ Aprovado' : '❌ Negado'
45
+ try {
46
+ await ctx.answerCallbackQuery({ text: label })
47
+ } catch (e: any) {
48
+ console.error(`[callback] answerCallbackQuery falhou: ${e.message}`)
49
+ }
23
50
  try {
24
51
  if (found) {
25
- await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } })
52
+ // Get the command text from the original message caption (plain text)
53
+ const origText = ctx.callbackQuery.message?.text ?? ''
54
+ // Replace keyboard with result label — rebuild safe HTML
55
+ const cmdLine = origText.split('\n').slice(1).join('\n').trim()
56
+ const escaped = cmdLine.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
26
57
  await ctx.editMessageText(
27
- (ctx.callbackQuery.message?.text ?? '') + `\n\n${label}`,
58
+ `🔧 <b>Codex quer executar:</b>\n<code>${escaped}</code>\n\n${label}`,
28
59
  { parse_mode: 'HTML' }
29
60
  )
30
61
  }
31
- await ctx.answerCallbackQuery({ text: label })
32
- } catch {}
62
+ } catch (e: any) {
63
+ console.error(`[callback] editMessageText falhou: ${e.message}`)
64
+ }
33
65
  })
34
66
 
35
67
  bot.catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cliclaw",
3
- "version": "1.0.26",
3
+ "version": "1.0.34",
4
4
  "description": "Telegram bot bridging AI CLIs (Claude Code, Codex) to Forum Topics",
5
5
  "main": "index.ts",
6
6
  "scripts": {
@@ -128,20 +128,30 @@ function handleProtoEvent(ps: ProtoSession, obj: any) {
128
128
  if (msg?.type === 'exec_approval_request') {
129
129
  const callId = msg.call_id ?? msgId
130
130
  const commandStr = formatApprovalCommand(msg.command ?? msg.cmd)
131
+ console.log(`[Codex approval] request recebido call_id=${callId} cmd="${commandStr.slice(0, 80)}"`)
131
132
  const respond = (approved: boolean) => {
133
+ console.log(`[Codex approval] enviando exec_approval call_id=${callId} approved=${approved}`)
132
134
  try {
133
135
  ps.proc.stdin!.write(JSON.stringify({
134
136
  id: randomUUID(),
135
- op: { type: 'exec_approval', call_id: callId, approved },
137
+ op: { type: 'exec_approval', id: callId, decision: approved ? 'approved' : 'denied' },
136
138
  }) + '\n')
137
- } catch {}
139
+ console.log(`[Codex approval] enviado ok`)
140
+ } catch (e: any) {
141
+ console.error(`[Codex approval] erro ao enviar: ${e.message}`)
142
+ }
138
143
  }
139
144
  if (ps.approvalHandler) {
145
+ console.log(`[Codex approval] aguardando usuário via Telegram...`)
140
146
  ps.approvalHandler(callId, commandStr)
141
147
  .then(respond)
142
- .catch(() => respond(false))
148
+ .catch((e: any) => {
149
+ console.error(`[Codex approval] handler error: ${e?.message} — negando`)
150
+ respond(false)
151
+ })
143
152
  } else {
144
- respond(true) // no handler auto-approve
153
+ console.log(`[Codex approval] sem handler auto-aprovando`)
154
+ respond(true)
145
155
  }
146
156
  return
147
157
  }
@@ -17,12 +17,18 @@ export function registerApproval(
17
17
  resolve: (approved: boolean) => void,
18
18
  ttlMs = 5 * 60 * 1000 // auto-deny after 5 min
19
19
  ): void {
20
- const timeout = setTimeout(() => respondApproval(id, false), ttlMs)
20
+ console.log(`[approvals] registrado id=${id.slice(0, 8)}... total pendentes=${pending.size + 1}`)
21
+ const timeout = setTimeout(() => {
22
+ console.log(`[approvals] timeout id=${id.slice(0, 8)}... auto-negando`)
23
+ respondApproval(id, false)
24
+ }, ttlMs)
21
25
  pending.set(id, { resolve, timeout })
22
26
  }
23
27
 
24
28
  /** Called by the callback-query handler when the user clicks Approve/Deny. */
25
29
  export function respondApproval(id: string, approved: boolean): boolean {
30
+ const found = pending.has(id)
31
+ console.log(`[approvals] respondApproval id=${id.slice(0, 8)}... approved=${approved} found=${found} total=${pending.size}`)
26
32
  const p = pending.get(id)
27
33
  if (!p) return false
28
34
  clearTimeout(p.timeout)
@@ -58,95 +58,106 @@ async function tryRenameThread(
58
58
  } catch {}
59
59
  }
60
60
 
61
- export function registerMessageHandler(bot: Bot<Context>, storage: Storage, config: Config) {
62
- bot.on('message:text', async (ctx) => {
63
- const chatId = String(ctx.chat.id)
64
- const text = ctx.message.text
65
- const threadId = ctx.message.message_thread_id ?? 0
66
- const lang = getLang(ctx)
67
-
68
- if (text.startsWith('/')) return
69
-
70
- let session: Session | null = null
71
- if (threadId > 0) {
72
- session = storage.getSessionByThreadId(chatId, threadId)
73
- if (!session) return
74
- } else {
75
- session = storage.getActiveSession(chatId)
76
- if (!session) {
77
- await ctx.reply(t(lang, 'noActiveSessionReply'))
78
- return
79
- }
80
- }
81
-
82
- if (activeLocks.has(session.id)) {
83
- const opts: any = threadId > 0 ? { message_thread_id: threadId } : {}
84
- await ctx.reply(t(lang, 'stillProcessing'), opts)
61
+ // ── long-running message logic (runs concurrently does NOT block grammY) ──
62
+ async function processMessage(ctx: Context, storage: Storage, config: Config) {
63
+ const chatId = String(ctx.chat!.id)
64
+ const text = ctx.message!.text!
65
+ const threadId = ctx.message!.message_thread_id ?? 0
66
+ const lang = getLang(ctx)
67
+
68
+ if (text.startsWith('/')) return
69
+
70
+ let session: Session | null = null
71
+ if (threadId > 0) {
72
+ session = storage.getSessionByThreadId(chatId, threadId)
73
+ if (!session) return
74
+ } else {
75
+ session = storage.getActiveSession(chatId)
76
+ if (!session) {
77
+ await ctx.reply(t(lang, 'noActiveSessionReply'))
85
78
  return
86
79
  }
80
+ }
87
81
 
88
- const isFirstMessage = session.history.length === 0
89
-
90
- activeLocks.add(session.id)
91
- const replyOpts: any = threadId > 0 ? { message_thread_id: threadId } : {}
92
- const emoji = session.model === 'claude' ? '🟣' : '🟢'
82
+ console.log(`[msg] chat=${chatId} thread=${threadId} model=${session.model} sid=${session.id.slice(0, 8)}... "${text.slice(0, 50).replace(/\n/g, '↵')}"`)
93
83
 
94
- const workingMsg = await ctx.reply(
95
- `${emoji} <i>${t(lang, 'processing')}</i>`,
96
- { ...replyOpts, parse_mode: 'HTML' }
97
- )
84
+ if (activeLocks.has(session.id)) {
85
+ // Session already processing — silently ignore (user sees "Processando..." in topic)
86
+ console.log(`[msg] ignorando processando sid=${session.id.slice(0, 8)}...`)
87
+ return
88
+ }
98
89
 
99
- const typingLoop = setInterval(async () => {
100
- try { await ctx.replyWithChatAction('typing', replyOpts) } catch {}
101
- }, 4000)
102
-
103
- storage.addMessage(chatId, session.id, 'user', text)
104
-
105
- // ── build codex approval handler (only if PERMISSION_MODE=ask) ─────────
106
- const codexApprovalHandler = (session.model === 'codex' && config.PERMISSION_MODE === '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
-
124
- let response: string
125
- try {
126
- let result: { text: string; usage?: TokenUsage }
127
- if (session.model === 'claude') {
128
- result = await askClaude(session, text,
129
- (id) => storage.setClaudeSessionId(chatId, session!.id, id))
130
- } else {
131
- result = await askCodex(session, text,
132
- (id) => storage.setCodexThreadId(chatId, session!.id, id),
133
- codexApprovalHandler)
90
+ const isFirstMessage = session.history.length === 0
91
+
92
+ activeLocks.add(session.id)
93
+ const replyOpts: any = threadId > 0 ? { message_thread_id: threadId } : {}
94
+ const emoji = session.model === 'claude' ? '🟣' : '🟢'
95
+
96
+ const workingMsg = await ctx.reply(
97
+ `${emoji} <i>${t(lang, 'processing')}</i>`,
98
+ { ...replyOpts, parse_mode: 'HTML' }
99
+ )
100
+
101
+ const typingLoop = setInterval(async () => {
102
+ try { await ctx.replyWithChatAction('typing', replyOpts) } catch {}
103
+ }, 4000)
104
+
105
+ storage.addMessage(chatId, session.id, 'user', text)
106
+
107
+ // ── build codex approval handler (only if PERMISSION_MODE=ask) ─────────
108
+ const codexApprovalHandler = (session.model === 'codex' && config.PERMISSION_MODE === 'ask')
109
+ ? (callId: string, cmdStr: string) => {
110
+ const approvalId = randomUUID()
111
+ ctx.api.sendMessage(chatId,
112
+ `🔧 <b>Codex quer executar:</b>\n<code>${escapeHtml(cmdStr)}</code>`,
113
+ {
114
+ ...replyOpts,
115
+ parse_mode: 'HTML',
116
+ reply_markup: { inline_keyboard: [[
117
+ { text: '✅ Aprovar', callback_data: `capprove:${approvalId}` },
118
+ { text: '❌ Negar', callback_data: `cdeny:${approvalId}` },
119
+ ]] },
120
+ }
121
+ ).catch(() => {})
122
+ return new Promise<boolean>((resolve) => registerApproval(approvalId, resolve))
134
123
  }
135
- response = (isFirstMessage && result.usage) ? result.text + formatUsage(result.usage) : result.text
136
- } catch (err: any) {
137
- response = `\u274c ${lang === 'pt' ? 'Erro' : 'Error'}: ${err.message}`
138
- } finally {
139
- clearInterval(typingLoop)
140
- activeLocks.delete(session.id)
124
+ : undefined
125
+
126
+ let response: string
127
+ try {
128
+ let result: { text: string; usage?: TokenUsage }
129
+ if (session.model === 'claude') {
130
+ result = await askClaude(session, text,
131
+ (id) => storage.setClaudeSessionId(chatId, session!.id, id))
132
+ } else {
133
+ result = await askCodex(session, text,
134
+ (id) => storage.setCodexThreadId(chatId, session!.id, id),
135
+ codexApprovalHandler)
141
136
  }
137
+ response = (isFirstMessage && result.usage) ? result.text + formatUsage(result.usage) : result.text
138
+ } catch (err: any) {
139
+ response = `\u274c ${lang === 'pt' ? 'Erro' : 'Error'}: ${err.message}`
140
+ } finally {
141
+ clearInterval(typingLoop)
142
+ activeLocks.delete(session.id)
143
+ }
142
144
 
143
- storage.addMessage(chatId, session.id, 'assistant', response)
145
+ storage.addMessage(chatId, session.id, 'assistant', response)
144
146
 
145
- if (isFirstMessage && config.FORUM_GROUP_ID && threadId > 0) {
146
- await tryRenameThread(ctx.api, Number(config.FORUM_GROUP_ID), threadId, text, session.model)
147
- }
147
+ if (isFirstMessage && config.FORUM_GROUP_ID && threadId > 0) {
148
+ await tryRenameThread(ctx.api, Number(config.FORUM_GROUP_ID), threadId, text, session.model)
149
+ }
150
+
151
+ try { await ctx.api.deleteMessage(ctx.chat!.id, workingMsg.message_id) } catch {}
152
+ await sendLong(ctx.api, chatId, `${emoji} ${response}`, replyOpts)
153
+ }
148
154
 
149
- try { await ctx.api.deleteMessage(ctx.chat.id, workingMsg.message_id) } catch {}
150
- await sendLong(ctx.api, chatId, `${emoji} ${response}`, replyOpts)
155
+ export function registerMessageHandler(bot: Bot<Context>, storage: Storage, config: Config) {
156
+ bot.on('message:text', (ctx) => {
157
+ // Fire-and-forget: return immediately so grammY can process other updates
158
+ // (e.g. callback_query for approval buttons) while waiting for AI response
159
+ void processMessage(ctx, storage, config).catch(err =>
160
+ console.error('[msg] erro não tratado:', err.message)
161
+ )
151
162
  })
152
163
  }