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 +38 -6
- package/package.json +1 -1
- package/src/agents/codex.ts +14 -4
- package/src/handlers/approvals.ts +7 -1
- package/src/handlers/messages.ts +91 -80
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
|
-
|
|
20
|
-
const
|
|
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
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
26
57
|
await ctx.editMessageText(
|
|
27
|
-
|
|
58
|
+
`🔧 <b>Codex quer executar:</b>\n<code>${escaped}</code>\n\n${label}`,
|
|
28
59
|
{ parse_mode: 'HTML' }
|
|
29
60
|
)
|
|
30
61
|
}
|
|
31
|
-
|
|
32
|
-
|
|
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
package/src/agents/codex.ts
CHANGED
|
@@ -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',
|
|
137
|
+
op: { type: 'exec_approval', id: callId, decision: approved ? 'approved' : 'denied' },
|
|
136
138
|
}) + '\n')
|
|
137
|
-
|
|
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(() =>
|
|
148
|
+
.catch((e: any) => {
|
|
149
|
+
console.error(`[Codex approval] handler error: ${e?.message} — negando`)
|
|
150
|
+
respond(false)
|
|
151
|
+
})
|
|
143
152
|
} else {
|
|
144
|
-
|
|
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
|
-
|
|
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)
|
package/src/handlers/messages.ts
CHANGED
|
@@ -58,95 +58,106 @@ async function tryRenameThread(
|
|
|
58
58
|
} catch {}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
if (activeLocks.has(session.id)) {
|
|
85
|
+
// Session already processing — silently ignore (user sees "Processando..." in topic)
|
|
86
|
+
console.log(`[msg] ignorando — já processando sid=${session.id.slice(0, 8)}...`)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
98
89
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
+
storage.addMessage(chatId, session.id, 'assistant', response)
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
}
|