cliclaw 1.0.15 → 1.0.17

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/agents/codex.ts +33 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cliclaw",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Telegram bot bridging AI CLIs (Claude Code, Codex) to Forum Topics",
5
5
  "main": "index.ts",
6
6
  "scripts": {
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'child_process'
2
- import type { Session } from '../storage'
2
+ import type { Session, Message } from '../storage'
3
3
  import type { TokenUsage } from './claude'
4
4
 
5
5
  const HOME = process.env.HOME || process.env.USERPROFILE || '/root'
@@ -16,26 +16,40 @@ const BASE_ENV = {
16
16
  }
17
17
 
18
18
  function extractTextFromObj(obj: any): string | null {
19
- // Format 1: item.completed with agent_message
19
+ // New codex format: {"id":"...","msg":{"type":"agent_message","text":"..."}}
20
+ if (obj.msg?.type === 'agent_message' && obj.msg?.text) return obj.msg.text
21
+ // Old format: item.completed with agent_message
20
22
  if (obj.type === 'item.completed' && obj.item?.type === 'agent_message' && obj.item?.text)
21
23
  return obj.item.text
22
- // Format 2: response.output_item.done with message content array
24
+ // OpenAI Responses API: response.output_item.done with message content array
23
25
  if (obj.item?.type === 'message' && Array.isArray(obj.item?.content)) {
24
26
  const parts = obj.item.content
25
27
  .filter((c: any) => c.type === 'output_text' && c.text)
26
28
  .map((c: any) => c.text)
27
29
  if (parts.length > 0) return parts.join('')
28
30
  }
29
- // Format 3: top-level result/output/text
31
+ // Fallback: top-level result/output
30
32
  if (typeof obj.result === 'string' && obj.result) return obj.result
31
33
  if (typeof obj.output === 'string' && obj.output) return obj.output
32
- if (typeof obj.text === 'string' && obj.text && obj.type !== 'thread.started') return obj.text
33
- // Format 4: message with content string
34
- if (obj.type === 'message' && typeof obj.content === 'string' && obj.content) return obj.content
35
34
  return null
36
35
  }
37
36
 
38
- function spawnCodex(args: string[]): Promise<{ text: string; threadId: string | null; usage?: TokenUsage }> {
37
+ function buildPrompt(history: Message[], userMessage: string): string {
38
+ // history already contains the current user message as the last item (added before askCodex)
39
+ const prior = history.slice(0, -1).slice(-20) // up to last 20 prior messages
40
+ if (prior.length === 0) return userMessage
41
+ const lines = prior.map(m => {
42
+ let content = m.content
43
+ // Strip usage stats suffix (e.g. "\n`📊 ↑1234 ↓56`")
44
+ const usageIdx = content.lastIndexOf('\n`📊')
45
+ if (usageIdx > 0) content = content.slice(0, usageIdx)
46
+ if (content.length > 600) content = content.slice(0, 600) + '…'
47
+ return `${m.role === 'user' ? 'User' : 'Assistant'}: ${content}`
48
+ })
49
+ return `<conversation_history>\n${lines.join('\n')}\n</conversation_history>\n\nUser: ${userMessage}`
50
+ }
51
+
52
+ function spawnCodex(args: string[], stdinText?: string): Promise<{ text: string; threadId: string | null; usage?: TokenUsage }> {
39
53
  return new Promise((resolve, reject) => {
40
54
  let stdout = ''
41
55
  let stderr = ''
@@ -45,8 +59,12 @@ function spawnCodex(args: string[]): Promise<{ text: string; threadId: string |
45
59
  cwd: process.cwd(),
46
60
  shell: isWin,
47
61
  windowsHide: true,
48
- stdio: ['ignore', 'pipe', 'pipe'], // explicit: ignore stdin so headless PM2 doesn't hang
62
+ // Use pipe for stdin when we have text to send; otherwise ignore to prevent PM2 hang
63
+ stdio: [stdinText != null ? 'pipe' : 'ignore', 'pipe', 'pipe'],
49
64
  })
65
+ if (stdinText != null) {
66
+ proc.stdin!.end(stdinText, 'utf8')
67
+ }
50
68
  proc.stdout.on('data', (d: Buffer) => { stdout += d.toString() })
51
69
  proc.stderr.on('data', (d: Buffer) => {
52
70
  const s = d.toString().trim()
@@ -102,12 +120,12 @@ export async function askCodex(
102
120
  onNewThreadId?: (id: string) => void
103
121
  ): Promise<{ text: string; usage?: TokenUsage }> {
104
122
  try {
105
- const codexThreadId = session.codexThreadId
106
- const args = codexThreadId
107
- ? ['exec', 'resume', codexThreadId, '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', userMessage]
108
- : ['exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', userMessage]
109
- const { text, threadId, usage } = await spawnCodex(args)
110
- if (threadId && !codexThreadId) onNewThreadId?.(threadId)
123
+ // Build full prompt with conversation history, send via stdin (avoids Windows arg escaping issues)
124
+ const prompt = buildPrompt(session.history, userMessage)
125
+ // `-` tells codex exec to read PROMPT from stdin
126
+ const args = ['exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--json', '-']
127
+ const { text, threadId, usage } = await spawnCodex(args, prompt)
128
+ if (threadId && !session.codexThreadId) onNewThreadId?.(threadId)
111
129
  return { text, usage }
112
130
  } catch (err: any) {
113
131
  return { text: `❌ Codex error: ${err.message}` }