cliclaw 1.0.34 → 1.0.36

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
@@ -25,37 +25,40 @@ registerCommands(bot, storage, config)
25
25
  registerMessageHandler(bot, storage, config)
26
26
 
27
27
  // ── Codex exec approval callbacks ──────────────────────────────────────────
28
+ const APPROVAL_PREFIXES: Record<string, { decision: string; label: string }> = {
29
+ capprove: { decision: 'approved', label: '✅ Aprovado' },
30
+ csession: { decision: 'approved_for_session', label: '🔓 Sessão aprovada!' },
31
+ cdeny: { decision: 'denied', label: '❌ Negado' },
32
+ cabort: { decision: 'abort', label: '🛑 Abortado' },
33
+ }
34
+
28
35
  bot.on('callback_query:data', async (ctx) => {
29
36
  const data = ctx.callbackQuery.data
30
37
  console.log(`[callback] recebido: ${data.slice(0, 60)}`)
31
38
 
32
- if (!data.startsWith('capprove:') && !data.startsWith('cdeny:')) return
33
-
34
- const colonIdx = data.indexOf(':')
35
- const prefix = data.slice(0, colonIdx)
39
+ const colonIdx = data.indexOf(':')
40
+ const prefix = data.slice(0, colonIdx)
36
41
  const approvalId = data.slice(colonIdx + 1)
37
- const approved = prefix === 'capprove'
42
+ const entry = APPROVAL_PREFIXES[prefix]
43
+ if (!entry) return
38
44
 
39
- console.log(`[callback] clicou ${approved ? 'APROVAR' : 'NEGAR'} id=${approvalId.slice(0, 8)}...`)
45
+ console.log(`[callback] clicou ${prefix} id=${approvalId.slice(0, 8)}...`)
40
46
 
41
- const found = respondApproval(approvalId, approved)
42
- console.log(`[callback] respondApproval found=${found}`)
47
+ const found = respondApproval(approvalId, entry.decision)
48
+ console.log(`[callback] respondApproval found=${found} decision=${entry.decision}`)
43
49
 
44
- const label = approved ? '✅ Aprovado' : '❌ Negado'
45
50
  try {
46
- await ctx.answerCallbackQuery({ text: label })
51
+ await ctx.answerCallbackQuery({ text: entry.label })
47
52
  } catch (e: any) {
48
53
  console.error(`[callback] answerCallbackQuery falhou: ${e.message}`)
49
54
  }
50
55
  try {
51
56
  if (found) {
52
- // Get the command text from the original message caption (plain text)
53
57
  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;')
58
+ const cmdLine = origText.split('\n').slice(1).join('\n').trim()
59
+ const escaped = cmdLine.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
57
60
  await ctx.editMessageText(
58
- `🔧 <b>Codex quer executar:</b>\n<code>${escaped}</code>\n\n${label}`,
61
+ `🔧 <b>Codex quer executar:</b>\n<code>${escaped}</code>\n\n${entry.label}`,
59
62
  { parse_mode: 'HTML' }
60
63
  )
61
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cliclaw",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Telegram bot bridging AI CLIs (Claude Code, Codex) to Forum Topics",
5
5
  "main": "index.ts",
6
6
  "scripts": {
@@ -26,12 +26,13 @@ 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
34
- approvalHandler?: (callId: string, commandStr: string) => Promise<boolean>
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<string>
35
+ sessionApproved: boolean // true after user clicks "Aprovar Sessão"
35
36
  }
36
37
 
37
38
  // Map from cliclaw session.id → ProtoSession
@@ -74,6 +75,7 @@ function startProto(appSessionId: string): ProtoSession {
74
75
  lastUsed: Date.now(),
75
76
  pending: new Map(),
76
77
  buf: '',
78
+ sessionApproved: false,
77
79
  }
78
80
 
79
81
  proc.stdout!.on('data', (d: Buffer) => {
@@ -126,32 +128,47 @@ function handleProtoEvent(ps: ProtoSession, obj: any) {
126
128
 
127
129
  // ── exec approval request ───────────────────────────────────────────────────
128
130
  if (msg?.type === 'exec_approval_request') {
129
- const callId = msg.call_id ?? msgId
131
+ const subId = msg.sub_id ?? msg.call_id ?? msgId
130
132
  const commandStr = formatApprovalCommand(msg.command ?? msg.cmd)
131
- console.log(`[Codex approval] request recebido call_id=${callId} cmd="${commandStr.slice(0, 80)}"`)
132
- const respond = (approved: boolean) => {
133
- console.log(`[Codex approval] enviando exec_approval call_id=${callId} approved=${approved}`)
133
+ console.log(`[Codex approval] request recebido sub_id=${subId} cmd="${commandStr.slice(0, 80)}"`)
134
+
135
+ // Respond using the original message id so Codex can correlate the reply
136
+ const respond = (decision: string) => {
137
+ // Handle app-level session approval — Codex only understands 'approved'/'denied'/'abort'
138
+ if (decision === 'approved_for_session') {
139
+ ps.sessionApproved = true
140
+ decision = 'approved'
141
+ }
142
+ console.log(`[Codex approval] enviando exec_approval sub_id=${subId} decision=${decision}`)
134
143
  try {
135
144
  ps.proc.stdin!.write(JSON.stringify({
136
- id: randomUUID(),
137
- op: { type: 'exec_approval', id: callId, decision: approved ? 'approved' : 'denied' },
145
+ id: msgId, // use original event id, not a new UUID
146
+ op: { type: 'exec_approval', sub_id: subId, decision },
138
147
  }) + '\n')
139
148
  console.log(`[Codex approval] enviado ok`)
140
149
  } catch (e: any) {
141
150
  console.error(`[Codex approval] erro ao enviar: ${e.message}`)
142
151
  }
143
152
  }
153
+
154
+ // If the user already approved the whole session, skip the prompt
155
+ if (ps.sessionApproved) {
156
+ console.log(`[Codex approval] sessão aprovada — auto-aprovando`)
157
+ respond('approved')
158
+ return
159
+ }
160
+
144
161
  if (ps.approvalHandler) {
145
162
  console.log(`[Codex approval] aguardando usuário via Telegram...`)
146
- ps.approvalHandler(callId, commandStr)
163
+ ps.approvalHandler(subId, commandStr)
147
164
  .then(respond)
148
165
  .catch((e: any) => {
149
166
  console.error(`[Codex approval] handler error: ${e?.message} — negando`)
150
- respond(false)
167
+ respond('denied')
151
168
  })
152
169
  } else {
153
170
  console.log(`[Codex approval] sem handler — auto-aprovando`)
154
- respond(true)
171
+ respond('approved')
155
172
  }
156
173
  return
157
174
  }
@@ -205,7 +222,7 @@ export async function askCodex(
205
222
  session: { id: string; codexThreadId?: string },
206
223
  userMessage: string,
207
224
  _onNewThreadId?: (id: string) => void,
208
- approvalHandler?: (callId: string, commandStr: string) => Promise<boolean>
225
+ approvalHandler?: (callId: string, commandStr: string) => Promise<string>
209
226
  ): Promise<{ text: string; usage?: TokenUsage }> {
210
227
  const ps = getOrStart(session.id)
211
228
  // Update handler every call (chat context may have changed)
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Shared state for pending Codex exec approval requests.
3
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.
4
+ * with Approve/Deny/Session/Abort buttons. The user's answer is routed back here.
5
5
  */
6
6
 
7
7
  interface PendingApproval {
8
- resolve: (approved: boolean) => void
8
+ resolve: (decision: string) => void
9
9
  timeout: ReturnType<typeof setTimeout>
10
10
  }
11
11
 
@@ -14,25 +14,25 @@ const pending = new Map<string, PendingApproval>()
14
14
  /** Store a pending approval that will resolve when the user responds. */
15
15
  export function registerApproval(
16
16
  id: string,
17
- resolve: (approved: boolean) => void,
17
+ resolve: (decision: string) => 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
21
  const timeout = setTimeout(() => {
22
22
  console.log(`[approvals] timeout id=${id.slice(0, 8)}... auto-negando`)
23
- respondApproval(id, false)
23
+ respondApproval(id, 'denied')
24
24
  }, ttlMs)
25
25
  pending.set(id, { resolve, timeout })
26
26
  }
27
27
 
28
- /** Called by the callback-query handler when the user clicks Approve/Deny. */
29
- export function respondApproval(id: string, approved: boolean): boolean {
28
+ /** Called by the callback-query handler when the user clicks a decision button. */
29
+ export function respondApproval(id: string, decision: string): boolean {
30
30
  const found = pending.has(id)
31
- console.log(`[approvals] respondApproval id=${id.slice(0, 8)}... approved=${approved} found=${found} total=${pending.size}`)
31
+ console.log(`[approvals] respondApproval id=${id.slice(0, 8)}... decision=${decision} found=${found} total=${pending.size}`)
32
32
  const p = pending.get(id)
33
33
  if (!p) return false
34
34
  clearTimeout(p.timeout)
35
35
  pending.delete(id)
36
- p.resolve(approved)
36
+ p.resolve(decision)
37
37
  return true
38
38
  }
@@ -113,13 +113,19 @@ async function processMessage(ctx: Context, storage: Storage, config: Config) {
113
113
  {
114
114
  ...replyOpts,
115
115
  parse_mode: 'HTML',
116
- reply_markup: { inline_keyboard: [[
117
- { text: '✅ Aprovar', callback_data: `capprove:${approvalId}` },
118
- { text: ' Negar', callback_data: `cdeny:${approvalId}` },
119
- ]] },
116
+ reply_markup: { inline_keyboard: [
117
+ [
118
+ { text: ' Aprovar', callback_data: `capprove:${approvalId}` },
119
+ { text: '🔓 Aprovar Sessão', callback_data: `csession:${approvalId}` },
120
+ ],
121
+ [
122
+ { text: '❌ Negar', callback_data: `cdeny:${approvalId}` },
123
+ { text: '🛑 Abortar', callback_data: `cabort:${approvalId}` },
124
+ ],
125
+ ] },
120
126
  }
121
127
  ).catch(() => {})
122
- return new Promise<boolean>((resolve) => registerApproval(approvalId, resolve))
128
+ return new Promise<string>((resolve) => registerApproval(approvalId, resolve))
123
129
  }
124
130
  : undefined
125
131