cliclaw 1.0.25 → 1.0.33
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 -11
- 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
|
@@ -121,13 +121,6 @@ function handleProtoEvent(ps: ProtoSession, obj: any) {
|
|
|
121
121
|
|
|
122
122
|
if (msg?.type === 'session_configured') {
|
|
123
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
124
|
return
|
|
132
125
|
}
|
|
133
126
|
|
|
@@ -135,20 +128,30 @@ function handleProtoEvent(ps: ProtoSession, obj: any) {
|
|
|
135
128
|
if (msg?.type === 'exec_approval_request') {
|
|
136
129
|
const callId = msg.call_id ?? msgId
|
|
137
130
|
const commandStr = formatApprovalCommand(msg.command ?? msg.cmd)
|
|
131
|
+
console.log(`[Codex approval] request recebido call_id=${callId} cmd="${commandStr.slice(0, 80)}"`)
|
|
138
132
|
const respond = (approved: boolean) => {
|
|
133
|
+
console.log(`[Codex approval] enviando exec_approval call_id=${callId} approved=${approved}`)
|
|
139
134
|
try {
|
|
140
135
|
ps.proc.stdin!.write(JSON.stringify({
|
|
141
136
|
id: randomUUID(),
|
|
142
|
-
op: { type: '
|
|
137
|
+
op: { type: 'exec_approval', id: callId, decision: approved ? 'approve' : 'deny' },
|
|
143
138
|
}) + '\n')
|
|
144
|
-
|
|
139
|
+
console.log(`[Codex approval] enviado ok`)
|
|
140
|
+
} catch (e: any) {
|
|
141
|
+
console.error(`[Codex approval] erro ao enviar: ${e.message}`)
|
|
142
|
+
}
|
|
145
143
|
}
|
|
146
144
|
if (ps.approvalHandler) {
|
|
145
|
+
console.log(`[Codex approval] aguardando usuário via Telegram...`)
|
|
147
146
|
ps.approvalHandler(callId, commandStr)
|
|
148
147
|
.then(respond)
|
|
149
|
-
.catch(() =>
|
|
148
|
+
.catch((e: any) => {
|
|
149
|
+
console.error(`[Codex approval] handler error: ${e?.message} — negando`)
|
|
150
|
+
respond(false)
|
|
151
|
+
})
|
|
150
152
|
} else {
|
|
151
|
-
|
|
153
|
+
console.log(`[Codex approval] sem handler — auto-aprovando`)
|
|
154
|
+
respond(true)
|
|
152
155
|
}
|
|
153
156
|
return
|
|
154
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
|
}
|