codex-to-im 0.1.0
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/LICENSE +21 -0
- package/README.md +163 -0
- package/README_CN.md +161 -0
- package/SECURITY.md +38 -0
- package/SKILL.md +79 -0
- package/config.env.example +106 -0
- package/dist/cli.mjs +182 -0
- package/dist/daemon.mjs +206122 -0
- package/dist/ui-server.mjs +7725 -0
- package/docs/codex-to-im-prd.md +424 -0
- package/docs/codex-to-im-shared-thread-design.md +572 -0
- package/docs/install-windows.md +287 -0
- package/package.json +55 -0
- package/references/setup-guides.md +329 -0
- package/references/token-validation.md +44 -0
- package/references/troubleshooting.md +88 -0
- package/references/usage.md +46 -0
- package/scripts/build.js +36 -0
- package/scripts/daemon.ps1 +16 -0
- package/scripts/daemon.sh +225 -0
- package/scripts/doctor.ps1 +27 -0
- package/scripts/doctor.sh +450 -0
- package/scripts/install-codex.sh +65 -0
- package/scripts/run-tests.js +44 -0
- package/scripts/supervisor-linux.sh +49 -0
- package/scripts/supervisor-macos.sh +154 -0
- package/scripts/supervisor-windows.ps1 +481 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
CTI_HOME="$HOME/.claude-to-im"
|
|
4
|
+
CONFIG_FILE="$CTI_HOME/config.env"
|
|
5
|
+
PID_FILE="$CTI_HOME/runtime/bridge.pid"
|
|
6
|
+
LOG_FILE="$CTI_HOME/logs/bridge.log"
|
|
7
|
+
|
|
8
|
+
PASS=0
|
|
9
|
+
FAIL=0
|
|
10
|
+
|
|
11
|
+
check() {
|
|
12
|
+
local label="$1"
|
|
13
|
+
local result="$2"
|
|
14
|
+
if [ "$result" = "0" ]; then
|
|
15
|
+
echo "[OK] $label"
|
|
16
|
+
PASS=$((PASS + 1))
|
|
17
|
+
else
|
|
18
|
+
echo "[FAIL] $label"
|
|
19
|
+
FAIL=$((FAIL + 1))
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# --- Node.js version ---
|
|
24
|
+
if command -v node &>/dev/null; then
|
|
25
|
+
NODE_VER=$(node -v | sed 's/v//' | cut -d. -f1)
|
|
26
|
+
if [ "$NODE_VER" -ge 20 ] 2>/dev/null; then
|
|
27
|
+
check "Node.js >= 20 (found v$(node -v | sed 's/v//'))" 0
|
|
28
|
+
else
|
|
29
|
+
check "Node.js >= 20 (found v$(node -v | sed 's/v//'), need >= 20)" 1
|
|
30
|
+
fi
|
|
31
|
+
else
|
|
32
|
+
check "Node.js installed" 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# --- Helper: read a value from config.env ---
|
|
36
|
+
get_config() { grep "^$1=" "$CONFIG_FILE" 2>/dev/null | head -1 | cut -d= -f2- | sed 's/^["'"'"']//;s/["'"'"']$//'; }
|
|
37
|
+
|
|
38
|
+
# --- Read runtime setting ---
|
|
39
|
+
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
40
|
+
CTI_RUNTIME=$(get_config CTI_RUNTIME)
|
|
41
|
+
CTI_RUNTIME="${CTI_RUNTIME:-claude}"
|
|
42
|
+
echo "Runtime: $CTI_RUNTIME"
|
|
43
|
+
echo ""
|
|
44
|
+
|
|
45
|
+
# --- Claude CLI available (claude/auto modes) ---
|
|
46
|
+
if [ "$CTI_RUNTIME" = "claude" ] || [ "$CTI_RUNTIME" = "auto" ]; then
|
|
47
|
+
# Resolve CLI path matching the daemon's checkCliCompatibility logic:
|
|
48
|
+
# - Version >= 2.x AND all required flags present
|
|
49
|
+
# - Skip candidates that fail either check (same as resolveClaudeCliPath)
|
|
50
|
+
CLAUDE_PATH=""
|
|
51
|
+
CLAUDE_VER=""
|
|
52
|
+
CLAUDE_COMPAT=1
|
|
53
|
+
REQUIRED_FLAGS="output-format input-format permission-mode setting-sources"
|
|
54
|
+
|
|
55
|
+
# Helper: check if a candidate passes both version and flags checks.
|
|
56
|
+
# Sets CLAUDE_PATH/CLAUDE_VER/CLAUDE_COMPAT on success.
|
|
57
|
+
try_candidate() {
|
|
58
|
+
local cand="$1"
|
|
59
|
+
[ -x "$cand" ] || return 1
|
|
60
|
+
local ver
|
|
61
|
+
ver=$("$cand" --version 2>/dev/null || true)
|
|
62
|
+
[ -z "$ver" ] && return 1
|
|
63
|
+
local major
|
|
64
|
+
major=$(echo "$ver" | sed -E -n 's/^[^0-9]*([0-9]+)\..*/\1/p' | head -1)
|
|
65
|
+
if [ -z "$major" ] || ! [ "$major" -ge 2 ] 2>/dev/null; then
|
|
66
|
+
echo " (skipping $cand — version $ver is too old, need >= 2.x)"
|
|
67
|
+
return 1
|
|
68
|
+
fi
|
|
69
|
+
# Version OK — check flags
|
|
70
|
+
local help_text
|
|
71
|
+
help_text=$("$cand" --help 2>&1 || true)
|
|
72
|
+
for flag in $REQUIRED_FLAGS; do
|
|
73
|
+
if ! echo "$help_text" | grep -q "$flag"; then
|
|
74
|
+
echo " (skipping $cand — version $ver OK but missing --$flag)"
|
|
75
|
+
return 1
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
78
|
+
# Fully compatible
|
|
79
|
+
CLAUDE_PATH="$cand"
|
|
80
|
+
CLAUDE_VER="$ver"
|
|
81
|
+
CLAUDE_COMPAT=0
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# 1. Explicit env var — if set, daemon uses it unconditionally (no fallback).
|
|
86
|
+
# Doctor must mirror this: report on this path only, never scan further.
|
|
87
|
+
CTI_EXE=$(get_config CTI_CLAUDE_CODE_EXECUTABLE 2>/dev/null || true)
|
|
88
|
+
if [ -n "$CTI_EXE" ]; then
|
|
89
|
+
if [ -x "$CTI_EXE" ]; then
|
|
90
|
+
if ! try_candidate "$CTI_EXE"; then
|
|
91
|
+
# Explicit path is set but incompatible — daemon WILL use it and fail.
|
|
92
|
+
# Report it as the selected CLI so the user sees the real problem.
|
|
93
|
+
CLAUDE_PATH="$CTI_EXE"
|
|
94
|
+
CLAUDE_VER=$("$CTI_EXE" --version 2>/dev/null || echo "unknown")
|
|
95
|
+
# CLAUDE_COMPAT stays 1 (incompatible) — checks below will report failure
|
|
96
|
+
fi
|
|
97
|
+
else
|
|
98
|
+
CLAUDE_PATH="$CTI_EXE"
|
|
99
|
+
CLAUDE_VER="(not executable)"
|
|
100
|
+
fi
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# 2. All PATH candidates (only if no explicit env var was set)
|
|
104
|
+
if [ -z "$CTI_EXE" ] && [ -z "$CLAUDE_PATH" ]; then
|
|
105
|
+
ALL_CLAUDES=$(which -a claude 2>/dev/null || true)
|
|
106
|
+
for cand in $ALL_CLAUDES; do
|
|
107
|
+
try_candidate "$cand" && break
|
|
108
|
+
done
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# 3. Well-known locations (only if no explicit env var was set)
|
|
112
|
+
if [ -z "$CTI_EXE" ] && [ -z "$CLAUDE_PATH" ]; then
|
|
113
|
+
for cand in \
|
|
114
|
+
"$HOME/.claude/local/claude" \
|
|
115
|
+
"$HOME/.local/bin/claude" \
|
|
116
|
+
"/usr/local/bin/claude" \
|
|
117
|
+
"/opt/homebrew/bin/claude" \
|
|
118
|
+
"$HOME/.npm-global/bin/claude"; do
|
|
119
|
+
try_candidate "$cand" && break
|
|
120
|
+
done
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
if [ -n "$CLAUDE_PATH" ] && [ "$CLAUDE_COMPAT" = "0" ]; then
|
|
124
|
+
check "Claude CLI compatible (${CLAUDE_VER} at ${CLAUDE_PATH})" 0
|
|
125
|
+
elif [ -n "$CLAUDE_PATH" ]; then
|
|
126
|
+
# Path found but incompatible (too old, missing flags, or not executable)
|
|
127
|
+
check "Claude CLI compatible (${CLAUDE_VER} at ${CLAUDE_PATH} — incompatible, see above)" 1
|
|
128
|
+
else
|
|
129
|
+
if [ "$CTI_RUNTIME" = "claude" ]; then
|
|
130
|
+
check "Claude CLI available (not found in PATH or common locations)" 1
|
|
131
|
+
else
|
|
132
|
+
check "Claude CLI available (not found — will use Codex fallback)" 0
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# --- Claude CLI authenticated ---
|
|
137
|
+
# Skip this check if third-party API credentials are configured in config.env.
|
|
138
|
+
# In that mode the bridge authenticates via ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN,
|
|
139
|
+
# not via `claude auth login`, so a missing interactive login is expected and harmless.
|
|
140
|
+
HAS_THIRD_PARTY_AUTH=false
|
|
141
|
+
if [ -f "$CONFIG_FILE" ] && grep -qE "^ANTHROPIC_(API_KEY|AUTH_TOKEN)=" "$CONFIG_FILE" 2>/dev/null; then
|
|
142
|
+
HAS_THIRD_PARTY_AUTH=true
|
|
143
|
+
fi
|
|
144
|
+
if [ -n "$CLAUDE_PATH" ] && [ "$CLAUDE_COMPAT" = "0" ]; then
|
|
145
|
+
if [ "$HAS_THIRD_PARTY_AUTH" = "true" ]; then
|
|
146
|
+
check "Claude CLI auth (skipped — using third-party API credentials from config.env)" 0
|
|
147
|
+
else
|
|
148
|
+
AUTH_OUT=$("$CLAUDE_PATH" auth status 2>&1 || true)
|
|
149
|
+
if echo "$AUTH_OUT" | grep -qiE 'loggedIn.*true|logged.in'; then
|
|
150
|
+
check "Claude CLI authenticated" 0
|
|
151
|
+
else
|
|
152
|
+
check "Claude CLI authenticated (run 'claude auth login')" 1
|
|
153
|
+
fi
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# --- ANTHROPIC_* env reachability ---
|
|
158
|
+
# Check whether ANTHROPIC_* vars are configured in config.env.
|
|
159
|
+
# This is what matters for the daemon — the current shell env is irrelevant
|
|
160
|
+
# because on macOS the daemon runs under launchd with only plist env vars.
|
|
161
|
+
HAS_ANTHROPIC_CONFIG=false
|
|
162
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
163
|
+
if grep -q "^ANTHROPIC_" "$CONFIG_FILE" 2>/dev/null; then
|
|
164
|
+
HAS_ANTHROPIC_CONFIG=true
|
|
165
|
+
fi
|
|
166
|
+
fi
|
|
167
|
+
if [ "$HAS_ANTHROPIC_CONFIG" = "true" ]; then
|
|
168
|
+
check "ANTHROPIC_* vars in config.env (third-party API provider)" 0
|
|
169
|
+
|
|
170
|
+
PLIST_FILE="$HOME/Library/LaunchAgents/com.claude-to-im.bridge.plist"
|
|
171
|
+
|
|
172
|
+
# On macOS, verify the launchd plist also has the vars
|
|
173
|
+
if [ "$(uname -s)" = "Darwin" ] && [ -f "$PLIST_FILE" ]; then
|
|
174
|
+
if grep -q "ANTHROPIC_" "$PLIST_FILE" 2>/dev/null; then
|
|
175
|
+
check "ANTHROPIC_* vars in launchd plist" 0
|
|
176
|
+
else
|
|
177
|
+
check "ANTHROPIC_* vars in launchd plist (NOT present — restart bridge to regenerate plist)" 1
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# If bridge is running, verify the LIVE process has the vars.
|
|
182
|
+
# The plist may be correct on disk but if the daemon hasn't been
|
|
183
|
+
# restarted since the plist was regenerated, it still runs with the
|
|
184
|
+
# old environment.
|
|
185
|
+
BRIDGE_PID=$(cat "$PID_FILE" 2>/dev/null || true)
|
|
186
|
+
if [ -n "$BRIDGE_PID" ] && kill -0 "$BRIDGE_PID" 2>/dev/null; then
|
|
187
|
+
# ps eww shows the process environment on macOS/Linux
|
|
188
|
+
PROC_ENV=$(ps eww -p "$BRIDGE_PID" 2>/dev/null || true)
|
|
189
|
+
if echo "$PROC_ENV" | grep -q "ANTHROPIC_"; then
|
|
190
|
+
check "Running bridge process has ANTHROPIC_* env vars" 0
|
|
191
|
+
else
|
|
192
|
+
check "Running bridge process has ANTHROPIC_* env vars (NOT in process env — restart the bridge)" 1
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
else
|
|
196
|
+
check "ANTHROPIC_* vars in config.env (not set — OK if using default Anthropic auth)" 0
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
# --- SDK cli.js resolvable ---
|
|
200
|
+
SDK_CLI=""
|
|
201
|
+
for candidate in \
|
|
202
|
+
"$SKILL_DIR/node_modules/@anthropic-ai/claude-agent-sdk/cli.js" \
|
|
203
|
+
"$SKILL_DIR/node_modules/@anthropic-ai/claude-agent-sdk/dist/cli.js"; do
|
|
204
|
+
if [ -f "$candidate" ]; then
|
|
205
|
+
SDK_CLI="$candidate"
|
|
206
|
+
break
|
|
207
|
+
fi
|
|
208
|
+
done
|
|
209
|
+
if [ -n "$SDK_CLI" ]; then
|
|
210
|
+
check "Claude SDK cli.js exists ($SDK_CLI)" 0
|
|
211
|
+
else
|
|
212
|
+
if [ "$CTI_RUNTIME" = "claude" ]; then
|
|
213
|
+
check "Claude SDK cli.js exists (not found — run 'npm install' in $SKILL_DIR)" 1
|
|
214
|
+
else
|
|
215
|
+
check "Claude SDK cli.js exists (not found — OK for auto/codex mode)" 0
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# --- Codex checks (codex/auto modes) ---
|
|
221
|
+
if [ "$CTI_RUNTIME" = "codex" ] || [ "$CTI_RUNTIME" = "auto" ]; then
|
|
222
|
+
if command -v codex &>/dev/null; then
|
|
223
|
+
CODEX_VER=$(codex --version 2>/dev/null || echo "unknown")
|
|
224
|
+
check "Codex CLI available (${CODEX_VER})" 0
|
|
225
|
+
else
|
|
226
|
+
if [ "$CTI_RUNTIME" = "codex" ]; then
|
|
227
|
+
check "Codex CLI available (not found in PATH)" 1
|
|
228
|
+
else
|
|
229
|
+
check "Codex CLI available (not found — will use Claude)" 0
|
|
230
|
+
fi
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
# Check @openai/codex-sdk
|
|
234
|
+
CODEX_SDK="$SKILL_DIR/node_modules/@openai/codex-sdk"
|
|
235
|
+
if [ -d "$CODEX_SDK" ]; then
|
|
236
|
+
check "@openai/codex-sdk installed" 0
|
|
237
|
+
else
|
|
238
|
+
if [ "$CTI_RUNTIME" = "codex" ]; then
|
|
239
|
+
check "@openai/codex-sdk installed (not found — run 'npm install' in $SKILL_DIR)" 1
|
|
240
|
+
else
|
|
241
|
+
check "@openai/codex-sdk installed (not found — OK for auto/claude mode)" 0
|
|
242
|
+
fi
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
# Check Codex auth: any of CTI_CODEX_API_KEY / CODEX_API_KEY / OPENAI_API_KEY,
|
|
246
|
+
# or `codex auth status` showing logged-in (interactive login).
|
|
247
|
+
CODEX_AUTH=1
|
|
248
|
+
if [ -n "${CTI_CODEX_API_KEY:-}" ] || [ -n "${CODEX_API_KEY:-}" ] || [ -n "${OPENAI_API_KEY:-}" ]; then
|
|
249
|
+
CODEX_AUTH=0
|
|
250
|
+
elif command -v codex &>/dev/null; then
|
|
251
|
+
CODEX_AUTH_OUT=$(codex auth status 2>&1 || true)
|
|
252
|
+
if echo "$CODEX_AUTH_OUT" | grep -qiE 'logged.in|authenticated'; then
|
|
253
|
+
CODEX_AUTH=0
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
if [ "$CODEX_AUTH" = "0" ]; then
|
|
257
|
+
check "Codex auth available (API key or login)" 0
|
|
258
|
+
else
|
|
259
|
+
if [ "$CTI_RUNTIME" = "codex" ]; then
|
|
260
|
+
check "Codex auth available (set OPENAI_API_KEY or run 'codex auth login')" 1
|
|
261
|
+
else
|
|
262
|
+
check "Codex auth available (not found — needed only for Codex fallback)" 0
|
|
263
|
+
fi
|
|
264
|
+
fi
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
# --- dist/daemon.mjs freshness ---
|
|
268
|
+
DAEMON_MJS="$SKILL_DIR/dist/daemon.mjs"
|
|
269
|
+
if [ -f "$DAEMON_MJS" ]; then
|
|
270
|
+
STALE_SRC=$(find "$SKILL_DIR/src" -name '*.ts' -newer "$DAEMON_MJS" 2>/dev/null | head -1)
|
|
271
|
+
if [ -z "$STALE_SRC" ]; then
|
|
272
|
+
check "dist/daemon.mjs is up to date" 0
|
|
273
|
+
else
|
|
274
|
+
check "dist/daemon.mjs is stale (src changed, run 'npm run build')" 1
|
|
275
|
+
fi
|
|
276
|
+
else
|
|
277
|
+
check "dist/daemon.mjs exists (not built — run 'npm run build')" 1
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# --- config.env exists ---
|
|
281
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
282
|
+
check "config.env exists" 0
|
|
283
|
+
else
|
|
284
|
+
check "config.env exists ($CONFIG_FILE not found)" 1
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# --- config.env permissions ---
|
|
288
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
289
|
+
PERMS=$(stat -f "%Lp" "$CONFIG_FILE" 2>/dev/null || stat -c "%a" "$CONFIG_FILE" 2>/dev/null || echo "unknown")
|
|
290
|
+
if [ "$PERMS" = "600" ]; then
|
|
291
|
+
check "config.env permissions are 600" 0
|
|
292
|
+
else
|
|
293
|
+
check "config.env permissions are 600 (currently $PERMS)" 1
|
|
294
|
+
fi
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# --- Load config for channel checks ---
|
|
298
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
299
|
+
CTI_CHANNELS=$(get_config CTI_ENABLED_CHANNELS)
|
|
300
|
+
|
|
301
|
+
# --- Telegram ---
|
|
302
|
+
if echo "$CTI_CHANNELS" | grep -q telegram; then
|
|
303
|
+
TG_TOKEN=$(get_config CTI_TG_BOT_TOKEN)
|
|
304
|
+
if [ -n "$TG_TOKEN" ]; then
|
|
305
|
+
TG_RESULT=$(curl -s --max-time 5 "https://api.telegram.org/bot${TG_TOKEN}/getMe" 2>/dev/null || echo '{"ok":false}')
|
|
306
|
+
if echo "$TG_RESULT" | grep -q '"ok":true'; then
|
|
307
|
+
check "Telegram bot token is valid" 0
|
|
308
|
+
else
|
|
309
|
+
check "Telegram bot token is valid (getMe failed)" 1
|
|
310
|
+
fi
|
|
311
|
+
else
|
|
312
|
+
check "Telegram bot token configured" 1
|
|
313
|
+
fi
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
# --- Feishu ---
|
|
317
|
+
if echo "$CTI_CHANNELS" | grep -q feishu; then
|
|
318
|
+
FS_APP_ID=$(get_config CTI_FEISHU_APP_ID)
|
|
319
|
+
FS_SECRET=$(get_config CTI_FEISHU_APP_SECRET)
|
|
320
|
+
FS_DOMAIN=$(get_config CTI_FEISHU_DOMAIN)
|
|
321
|
+
FS_DOMAIN="${FS_DOMAIN:-https://open.feishu.cn}"
|
|
322
|
+
if [ -n "$FS_APP_ID" ] && [ -n "$FS_SECRET" ]; then
|
|
323
|
+
FEISHU_RESULT=$(curl -s --max-time 5 -X POST "${FS_DOMAIN}/open-apis/auth/v3/tenant_access_token/internal" \
|
|
324
|
+
-H "Content-Type: application/json" \
|
|
325
|
+
-d "{\"app_id\":\"${FS_APP_ID}\",\"app_secret\":\"${FS_SECRET}\"}" 2>/dev/null || echo '{"code":1}')
|
|
326
|
+
if echo "$FEISHU_RESULT" | grep -q '"code"[[:space:]]*:[[:space:]]*0'; then
|
|
327
|
+
check "Feishu app credentials are valid" 0
|
|
328
|
+
else
|
|
329
|
+
check "Feishu app credentials are valid (token request failed)" 1
|
|
330
|
+
fi
|
|
331
|
+
else
|
|
332
|
+
check "Feishu app credentials configured" 1
|
|
333
|
+
fi
|
|
334
|
+
fi
|
|
335
|
+
|
|
336
|
+
# --- QQ ---
|
|
337
|
+
if echo "$CTI_CHANNELS" | grep -q qq; then
|
|
338
|
+
QQ_APP_ID=$(get_config CTI_QQ_APP_ID)
|
|
339
|
+
QQ_APP_SECRET=$(get_config CTI_QQ_APP_SECRET)
|
|
340
|
+
if [ -n "$QQ_APP_ID" ] && [ -n "$QQ_APP_SECRET" ]; then
|
|
341
|
+
QQ_TOKEN_RESULT=$(curl -s --max-time 10 -X POST "https://bots.qq.com/app/getAppAccessToken" \
|
|
342
|
+
-H "Content-Type: application/json" \
|
|
343
|
+
-d "{\"appId\":\"${QQ_APP_ID}\",\"clientSecret\":\"${QQ_APP_SECRET}\"}" 2>/dev/null || echo '{}')
|
|
344
|
+
QQ_ACCESS_TOKEN=$(echo "$QQ_TOKEN_RESULT" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
345
|
+
if [ -n "$QQ_ACCESS_TOKEN" ]; then
|
|
346
|
+
check "QQ app credentials are valid (access_token obtained)" 0
|
|
347
|
+
# Verify gateway availability
|
|
348
|
+
QQ_GW_RESULT=$(curl -s --max-time 10 "https://api.sgroup.qq.com/gateway" \
|
|
349
|
+
-H "Authorization: QQBot ${QQ_ACCESS_TOKEN}" 2>/dev/null || echo '{}')
|
|
350
|
+
if echo "$QQ_GW_RESULT" | grep -q '"url"'; then
|
|
351
|
+
check "QQ gateway is reachable" 0
|
|
352
|
+
else
|
|
353
|
+
check "QQ gateway is reachable (GET /gateway failed)" 1
|
|
354
|
+
fi
|
|
355
|
+
else
|
|
356
|
+
check "QQ app credentials are valid (getAppAccessToken failed)" 1
|
|
357
|
+
fi
|
|
358
|
+
else
|
|
359
|
+
check "QQ app credentials configured" 1
|
|
360
|
+
fi
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# --- Discord ---
|
|
364
|
+
if echo "$CTI_CHANNELS" | grep -q discord; then
|
|
365
|
+
DC_TOKEN=$(get_config CTI_DISCORD_BOT_TOKEN)
|
|
366
|
+
if [ -n "$DC_TOKEN" ]; then
|
|
367
|
+
if echo "${DC_TOKEN}" | grep -qE '^[A-Za-z0-9_-]{20,}\.'; then
|
|
368
|
+
check "Discord bot token format" 0
|
|
369
|
+
else
|
|
370
|
+
check "Discord bot token format (does not match expected pattern)" 1
|
|
371
|
+
fi
|
|
372
|
+
else
|
|
373
|
+
check "Discord bot token configured" 1
|
|
374
|
+
fi
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
# --- Weixin ---
|
|
378
|
+
if echo "$CTI_CHANNELS" | grep -q weixin; then
|
|
379
|
+
WX_ACCOUNTS_FILE="$CTI_HOME/data/weixin-accounts.json"
|
|
380
|
+
if [ -f "$WX_ACCOUNTS_FILE" ]; then
|
|
381
|
+
WX_COUNTS=$(node -e '
|
|
382
|
+
const fs = require("fs");
|
|
383
|
+
const file = process.argv[1];
|
|
384
|
+
const accounts = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
385
|
+
const enabled = accounts.filter((a) => a && a.enabled && a.token).length;
|
|
386
|
+
process.stdout.write(`${enabled}:${accounts.length}`);
|
|
387
|
+
' "$WX_ACCOUNTS_FILE" 2>/dev/null || echo "0:0")
|
|
388
|
+
WX_ENABLED="${WX_COUNTS%%:*}"
|
|
389
|
+
WX_TOTAL="${WX_COUNTS##*:}"
|
|
390
|
+
if [ "${WX_ENABLED:-0}" -ge 1 ] 2>/dev/null; then
|
|
391
|
+
if [ "${WX_TOTAL:-0}" -gt 1 ] 2>/dev/null; then
|
|
392
|
+
check "Weixin linked account store (single-account mode; ${WX_TOTAL} records on disk, newest enabled record will be used)" 0
|
|
393
|
+
else
|
|
394
|
+
check "Weixin linked account store (single linked account ready)" 0
|
|
395
|
+
fi
|
|
396
|
+
else
|
|
397
|
+
check "Weixin linked account store (found file, but no enabled linked account with token — run 'cd $SKILL_DIR && npm run weixin:login')" 1
|
|
398
|
+
fi
|
|
399
|
+
else
|
|
400
|
+
check "Weixin linked account store (missing — run 'cd $SKILL_DIR && npm run weixin:login')" 1
|
|
401
|
+
fi
|
|
402
|
+
fi
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
# --- Log directory writable ---
|
|
406
|
+
LOG_DIR="$CTI_HOME/logs"
|
|
407
|
+
if [ -d "$LOG_DIR" ] && [ -w "$LOG_DIR" ]; then
|
|
408
|
+
check "Log directory is writable" 0
|
|
409
|
+
else
|
|
410
|
+
check "Log directory is writable ($LOG_DIR)" 1
|
|
411
|
+
fi
|
|
412
|
+
|
|
413
|
+
# --- PID file consistency ---
|
|
414
|
+
if [ -f "$PID_FILE" ]; then
|
|
415
|
+
PID=$(cat "$PID_FILE")
|
|
416
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
417
|
+
check "PID file consistent (process $PID is running)" 0
|
|
418
|
+
else
|
|
419
|
+
check "PID file consistent (stale PID $PID, process not running)" 1
|
|
420
|
+
fi
|
|
421
|
+
else
|
|
422
|
+
check "PID file consistency (no PID file, OK)" 0
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# --- Recent errors in log ---
|
|
426
|
+
if [ -f "$LOG_FILE" ]; then
|
|
427
|
+
ERROR_COUNT=$(tail -50 "$LOG_FILE" | grep -ciE 'ERROR|Fatal' || true)
|
|
428
|
+
if [ "$ERROR_COUNT" -eq 0 ]; then
|
|
429
|
+
check "No recent errors in log (last 50 lines)" 0
|
|
430
|
+
else
|
|
431
|
+
check "No recent errors in log (found $ERROR_COUNT ERROR/Fatal lines)" 1
|
|
432
|
+
fi
|
|
433
|
+
else
|
|
434
|
+
check "Log file exists (not yet created)" 0
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
echo ""
|
|
438
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
439
|
+
|
|
440
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
441
|
+
echo ""
|
|
442
|
+
echo "Common fixes:"
|
|
443
|
+
echo " SDK cli.js missing → cd $SKILL_DIR && npm install"
|
|
444
|
+
echo " dist/daemon.mjs stale → cd $SKILL_DIR && npm run build"
|
|
445
|
+
echo " config.env missing → run setup wizard"
|
|
446
|
+
echo " Weixin linked account missing→ cd $SKILL_DIR && npm run weixin:login"
|
|
447
|
+
echo " Stale PID file → run stop, then start"
|
|
448
|
+
fi
|
|
449
|
+
|
|
450
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Install the optional codex-to-im integration for Codex.
|
|
5
|
+
# Usage: bash scripts/install-codex.sh [--link]
|
|
6
|
+
# --link Create a symlink instead of copying (for development)
|
|
7
|
+
|
|
8
|
+
INTEGRATION_NAME="codex-to-im"
|
|
9
|
+
CODEX_SKILLS_DIR="$HOME/.codex/skills"
|
|
10
|
+
TARGET_DIR="$CODEX_SKILLS_DIR/$INTEGRATION_NAME"
|
|
11
|
+
SOURCE_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
echo "Installing optional $INTEGRATION_NAME integration for Codex..."
|
|
14
|
+
|
|
15
|
+
# Check source
|
|
16
|
+
if [ ! -f "$SOURCE_DIR/SKILL.md" ]; then
|
|
17
|
+
echo "Error: SKILL.md not found in $SOURCE_DIR"
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Create skills directory
|
|
22
|
+
mkdir -p "$CODEX_SKILLS_DIR"
|
|
23
|
+
|
|
24
|
+
# Check if already installed
|
|
25
|
+
if [ -e "$TARGET_DIR" ]; then
|
|
26
|
+
if [ -L "$TARGET_DIR" ]; then
|
|
27
|
+
EXISTING=$(readlink "$TARGET_DIR")
|
|
28
|
+
echo "Already installed as symlink → $EXISTING"
|
|
29
|
+
echo "To reinstall, remove it first: rm $TARGET_DIR"
|
|
30
|
+
exit 0
|
|
31
|
+
else
|
|
32
|
+
echo "Already installed at $TARGET_DIR"
|
|
33
|
+
echo "To reinstall, remove it first: rm -rf $TARGET_DIR"
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [ "${1:-}" = "--link" ]; then
|
|
39
|
+
ln -s "$SOURCE_DIR" "$TARGET_DIR"
|
|
40
|
+
echo "Symlinked: $TARGET_DIR → $SOURCE_DIR"
|
|
41
|
+
else
|
|
42
|
+
cp -R "$SOURCE_DIR" "$TARGET_DIR"
|
|
43
|
+
echo "Copied to: $TARGET_DIR"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Ensure dependencies (need devDependencies for build step)
|
|
47
|
+
if [ ! -d "$TARGET_DIR/node_modules" ] || [ ! -d "$TARGET_DIR/node_modules/@openai/codex-sdk" ]; then
|
|
48
|
+
echo "Installing dependencies..."
|
|
49
|
+
(cd "$TARGET_DIR" && npm install)
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Ensure build
|
|
53
|
+
if [ ! -f "$TARGET_DIR/dist/daemon.mjs" ]; then
|
|
54
|
+
echo "Building daemon bundle..."
|
|
55
|
+
(cd "$TARGET_DIR" && npm run build)
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Prune devDependencies after build
|
|
59
|
+
echo "Pruning dev dependencies..."
|
|
60
|
+
(cd "$TARGET_DIR" && npm prune --production)
|
|
61
|
+
|
|
62
|
+
echo ""
|
|
63
|
+
echo "Done! Start a new Codex session and use:"
|
|
64
|
+
echo " open codex-to-im — open the local workbench"
|
|
65
|
+
echo " share current session to Feishu — open the Feishu handoff flow"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-to-im-test-'));
|
|
7
|
+
const testsDir = path.join(process.cwd(), 'src', '__tests__');
|
|
8
|
+
const testFiles = fs.readdirSync(testsDir)
|
|
9
|
+
.filter((name) => name.endsWith('.test.ts'))
|
|
10
|
+
.map((name) => path.join('src', '__tests__', name));
|
|
11
|
+
|
|
12
|
+
const child = spawn(
|
|
13
|
+
process.execPath,
|
|
14
|
+
[
|
|
15
|
+
'--test',
|
|
16
|
+
'--test-concurrency=1',
|
|
17
|
+
'--import',
|
|
18
|
+
'tsx',
|
|
19
|
+
'--test-timeout=15000',
|
|
20
|
+
...testFiles,
|
|
21
|
+
],
|
|
22
|
+
{
|
|
23
|
+
stdio: 'inherit',
|
|
24
|
+
env: {
|
|
25
|
+
...process.env,
|
|
26
|
+
CTI_HOME: tempHome,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
child.on('exit', (code, signal) => {
|
|
32
|
+
try {
|
|
33
|
+
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
34
|
+
} catch {
|
|
35
|
+
// ignore
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (signal) {
|
|
39
|
+
process.kill(process.pid, signal);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
process.exit(code ?? 1);
|
|
44
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Linux supervisor — setsid/nohup fallback process management.
|
|
3
|
+
# Sourced by daemon.sh; expects CTI_HOME, SKILL_DIR, PID_FILE, STATUS_FILE, LOG_FILE.
|
|
4
|
+
|
|
5
|
+
# ── Public interface (called by daemon.sh) ──
|
|
6
|
+
|
|
7
|
+
supervisor_start() {
|
|
8
|
+
if command -v setsid >/dev/null 2>&1; then
|
|
9
|
+
setsid node "$SKILL_DIR/dist/daemon.mjs" >> "$LOG_FILE" 2>&1 < /dev/null &
|
|
10
|
+
else
|
|
11
|
+
nohup node "$SKILL_DIR/dist/daemon.mjs" >> "$LOG_FILE" 2>&1 < /dev/null &
|
|
12
|
+
fi
|
|
13
|
+
# Fallback: write shell $! as PID; main.ts will overwrite with real PID
|
|
14
|
+
echo $! > "$PID_FILE"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
supervisor_stop() {
|
|
18
|
+
local pid
|
|
19
|
+
pid=$(read_pid)
|
|
20
|
+
if [ -z "$pid" ]; then echo "No bridge running"; return 0; fi
|
|
21
|
+
if pid_alive "$pid"; then
|
|
22
|
+
kill "$pid"
|
|
23
|
+
for _ in $(seq 1 10); do
|
|
24
|
+
pid_alive "$pid" || break
|
|
25
|
+
sleep 1
|
|
26
|
+
done
|
|
27
|
+
pid_alive "$pid" && kill -9 "$pid"
|
|
28
|
+
echo "Bridge stopped"
|
|
29
|
+
else
|
|
30
|
+
echo "Bridge was not running (stale PID file)"
|
|
31
|
+
fi
|
|
32
|
+
rm -f "$PID_FILE"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
supervisor_is_managed() {
|
|
36
|
+
# Linux fallback has no service manager; always false
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
supervisor_status_extra() {
|
|
41
|
+
# No extra status for Linux fallback
|
|
42
|
+
:
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
supervisor_is_running() {
|
|
46
|
+
local pid
|
|
47
|
+
pid=$(read_pid)
|
|
48
|
+
pid_alive "$pid"
|
|
49
|
+
}
|