cliclaw 1.0.21 → 1.0.23
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 +21 -0
- package/package.json +48 -48
- package/src/agents/codex.ts +49 -8
- package/src/config.ts +6 -0
- package/src/handlers/approvals.ts +32 -0
- package/src/handlers/messages.ts +27 -1
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.
|
|
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.23",
|
|
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
|
+
}
|
package/src/agents/codex.ts
CHANGED
|
@@ -26,16 +26,25 @@ interface PendingReq {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
interface ProtoSession {
|
|
29
|
-
proc:
|
|
30
|
-
sessionId:
|
|
31
|
-
lastUsed:
|
|
32
|
-
pending:
|
|
33
|
-
buf:
|
|
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
|
|
|
@@ -161,11 +199,14 @@ function getOrStart(appSessionId: string): ProtoSession {
|
|
|
161
199
|
|
|
162
200
|
// ─── public API ──────────────────────────────────────────────────────────────
|
|
163
201
|
export async function askCodex(
|
|
164
|
-
session:
|
|
165
|
-
userMessage:
|
|
166
|
-
_onNewThreadId?: (id: string) => void
|
|
202
|
+
session: { id: string; codexThreadId?: string },
|
|
203
|
+
userMessage: string,
|
|
204
|
+
_onNewThreadId?: (id: string) => void,
|
|
205
|
+
approvalHandler?: (callId: string, commandStr: string) => Promise<boolean>
|
|
167
206
|
): Promise<{ text: string; usage?: TokenUsage }> {
|
|
168
207
|
const ps = getOrStart(session.id)
|
|
208
|
+
// Update handler every call (chat context may have changed)
|
|
209
|
+
if (approvalHandler !== undefined) ps.approvalHandler = approvalHandler
|
|
169
210
|
const msgId = randomUUID()
|
|
170
211
|
|
|
171
212
|
return new Promise((resolve, reject) => {
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir } from 'os'
|
|
|
5
5
|
|
|
6
6
|
export type AgentName = 'claude' | 'codex'
|
|
7
7
|
export type PermissionMode = 'auto' | 'session' | 'ask'
|
|
8
|
+
export type CodexApproval = 'ask' | 'allow'
|
|
8
9
|
|
|
9
10
|
export interface Config {
|
|
10
11
|
TELEGRAM_BOT_TOKEN: string
|
|
@@ -12,6 +13,7 @@ export interface Config {
|
|
|
12
13
|
FORUM_GROUP_ID: string
|
|
13
14
|
TELEGRAM_ADMIN_IDS: string[]
|
|
14
15
|
PERMISSION_MODE: PermissionMode
|
|
16
|
+
CODEX_APPROVAL: CodexApproval
|
|
15
17
|
availableAgents: AgentName[]
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -59,6 +61,9 @@ export function loadConfig(): Config {
|
|
|
59
61
|
rawMode === 'session' ? 'session' :
|
|
60
62
|
rawMode === 'ask' ? 'ask' : 'auto'
|
|
61
63
|
|
|
64
|
+
const rawApproval = (process.env.CODEX_APPROVAL || 'ask').toLowerCase()
|
|
65
|
+
const codexApproval: CodexApproval = rawApproval === 'allow' ? 'allow' : 'ask'
|
|
66
|
+
|
|
62
67
|
const config: Config = {
|
|
63
68
|
TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN || '',
|
|
64
69
|
DATA_DIR: process.env.DATA_DIR || join(USER_DIR, 'data'),
|
|
@@ -66,6 +71,7 @@ export function loadConfig(): Config {
|
|
|
66
71
|
TELEGRAM_ADMIN_IDS: (process.env.TELEGRAM_ADMIN_IDS || '')
|
|
67
72
|
.split(',').map(id => id.trim()).filter(Boolean),
|
|
68
73
|
PERMISSION_MODE: permMode,
|
|
74
|
+
CODEX_APPROVAL: codexApproval,
|
|
69
75
|
availableAgents: checkAgents(),
|
|
70
76
|
}
|
|
71
77
|
|
|
@@ -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
|
+
}
|
package/src/handlers/messages.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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'
|
|
@@ -5,6 +6,11 @@ import { askClaude, type TokenUsage } from '../agents/claude'
|
|
|
5
6
|
import { askCodex } 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
13
|
+
}
|
|
8
14
|
|
|
9
15
|
function formatUsage(usage: TokenUsage): string {
|
|
10
16
|
const fmt = (n: number) => n.toLocaleString('en')
|
|
@@ -96,6 +102,25 @@ export function registerMessageHandler(bot: Bot<Context>, storage: Storage, conf
|
|
|
96
102
|
|
|
97
103
|
storage.addMessage(chatId, session.id, 'user', text)
|
|
98
104
|
|
|
105
|
+
// ── build codex approval handler (only if CODEX_APPROVAL=ask) ──────────
|
|
106
|
+
const codexApprovalHandler = (session.model === 'codex' && config.CODEX_APPROVAL === '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
|
+
|
|
99
124
|
let response: string
|
|
100
125
|
try {
|
|
101
126
|
let result: { text: string; usage?: TokenUsage }
|
|
@@ -104,7 +129,8 @@ export function registerMessageHandler(bot: Bot<Context>, storage: Storage, conf
|
|
|
104
129
|
(id) => storage.setClaudeSessionId(chatId, session!.id, id))
|
|
105
130
|
} else {
|
|
106
131
|
result = await askCodex(session, text,
|
|
107
|
-
(id) => storage.setCodexThreadId(chatId, session!.id, id)
|
|
132
|
+
(id) => storage.setCodexThreadId(chatId, session!.id, id),
|
|
133
|
+
codexApprovalHandler)
|
|
108
134
|
}
|
|
109
135
|
response = (isFirstMessage && result.usage) ? result.text + formatUsage(result.usage) : result.text
|
|
110
136
|
} catch (err: any) {
|