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.
@@ -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
+ }