cliclaw 1.0.20 → 1.0.22
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/bin/cli.js +3 -2
- package/index.ts +21 -0
- package/package.json +48 -48
- package/src/agents/codex.ts +52 -5
- package/src/handlers/approvals.ts +32 -0
- package/src/handlers/messages.ts +28 -1
package/bin/cli.js
CHANGED
|
@@ -301,11 +301,12 @@ function cmdUpdate() {
|
|
|
301
301
|
if (!tgzName) throw new Error('npm pack did not produce a .tgz file')
|
|
302
302
|
|
|
303
303
|
// 2. Extract using the built-in Windows tar (available since Win10 1803)
|
|
304
|
+
// Use relative paths + cwd so tar doesn't misinterpret "C:" as a network host.
|
|
304
305
|
const extractDir = path.join(tmpDir, 'x')
|
|
305
306
|
fs.mkdirSync(extractDir)
|
|
306
307
|
const winTar = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
|
|
307
|
-
execSync(`"${winTar}" -xzf "${
|
|
308
|
-
{ stdio: 'pipe' })
|
|
308
|
+
execSync(`"${winTar}" -xzf "${tgzName}" -C "x"`,
|
|
309
|
+
{ stdio: 'pipe', cwd: tmpDir })
|
|
309
310
|
|
|
310
311
|
// 3. Copy files into the existing directory (no directory rename = no EBUSY)
|
|
311
312
|
const pkgDir = path.join(extractDir, 'package')
|
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.22",
|
|
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
|
|
|
@@ -199,6 +237,15 @@ export async function askCodex(
|
|
|
199
237
|
)
|
|
200
238
|
}
|
|
201
239
|
|
|
240
|
+
/** Attach a per-session handler that is called whenever codex wants to run a command. */
|
|
241
|
+
export function setCodexApprovalHandler(
|
|
242
|
+
appSessionId: string,
|
|
243
|
+
handler: (callId: string, commandStr: string) => Promise<boolean>
|
|
244
|
+
): void {
|
|
245
|
+
const ps = protoSessions.get(appSessionId)
|
|
246
|
+
if (ps) ps.approvalHandler = handler
|
|
247
|
+
}
|
|
248
|
+
|
|
202
249
|
export function killCodexSession(appSessionId: string) {
|
|
203
250
|
const ps = protoSessions.get(appSessionId)
|
|
204
251
|
if (ps) {
|
|
@@ -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,10 +1,16 @@
|
|
|
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'
|
|
4
5
|
import { askClaude, type TokenUsage } from '../agents/claude'
|
|
5
|
-
import { askCodex } from '../agents/codex'
|
|
6
|
+
import { askCodex, setCodexApprovalHandler } 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,27 @@ export function registerMessageHandler(bot: Bot<Context>, storage: Storage, conf
|
|
|
96
102
|
|
|
97
103
|
storage.addMessage(chatId, session.id, 'user', text)
|
|
98
104
|
|
|
105
|
+
// ── Codex approval handler ──────────────────────────────────────────────
|
|
106
|
+
if (session.model === 'codex') {
|
|
107
|
+
setCodexApprovalHandler(session.id, (callId, cmdStr) => {
|
|
108
|
+
const approvalId = randomUUID()
|
|
109
|
+
const label = `🔧 <b>Codex quer executar:</b>\n<code>${escapeHtml(cmdStr)}</code>`
|
|
110
|
+
ctx.api.sendMessage(chatId, label, {
|
|
111
|
+
...replyOpts,
|
|
112
|
+
parse_mode: 'HTML',
|
|
113
|
+
reply_markup: {
|
|
114
|
+
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) => {
|
|
121
|
+
registerApproval(approvalId, resolve)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
99
126
|
let response: string
|
|
100
127
|
try {
|
|
101
128
|
let result: { text: string; usage?: TokenUsage }
|