codelark 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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/SECURITY.md +34 -0
  4. package/SKILL.md +67 -0
  5. package/agents/openai.yaml +4 -0
  6. package/dist/cli.mjs +8794 -0
  7. package/dist/daemon.mjs +47172 -0
  8. package/dist/ui-server.mjs +22165 -0
  9. package/package.json +73 -0
  10. package/schemas/config.v1.schema.json +259 -0
  11. package/schemas/data/audit.v1.schema.json +44 -0
  12. package/schemas/data/auto-tasks.v1.schema.json +94 -0
  13. package/schemas/data/channel-chats.v1.schema.json +159 -0
  14. package/schemas/data/channel-default-targets.v1.schema.json +43 -0
  15. package/schemas/data/messages.v1.schema.json +23 -0
  16. package/schemas/data/number-map.v1.schema.json +9 -0
  17. package/schemas/data/permissions.v1.schema.json +35 -0
  18. package/schemas/data/sessions.v1.schema.json +330 -0
  19. package/schemas/data/string-map.v1.schema.json +9 -0
  20. package/schemas/manifest.json +121 -0
  21. package/scripts/analyze-bridge-log.js +838 -0
  22. package/scripts/build-preflight.d.ts +21 -0
  23. package/scripts/build-preflight.js +70 -0
  24. package/scripts/build.js +53 -0
  25. package/scripts/check-npm-pack.js +46 -0
  26. package/scripts/daemon.ps1 +16 -0
  27. package/scripts/daemon.sh +206 -0
  28. package/scripts/doctor.ps1 +27 -0
  29. package/scripts/doctor.sh +185 -0
  30. package/scripts/hot-update-bridge.sh +298 -0
  31. package/scripts/install-codex-skills.sh +127 -0
  32. package/scripts/install-codex.sh +10 -0
  33. package/scripts/migrate-bindings-to-channel-chats.js +228 -0
  34. package/scripts/patch-codex-sdk-windows-hide.js +96 -0
  35. package/scripts/real-feishu-e2e.ts +5804 -0
  36. package/scripts/run-tests.js +83 -0
  37. package/scripts/setup-wizard-real-e2e.ts +195 -0
  38. package/scripts/supervisor-linux.sh +49 -0
  39. package/scripts/supervisor-macos.sh +167 -0
  40. package/scripts/supervisor-windows.ps1 +481 -0
  41. package/skills/codelark/SKILL.md +67 -0
  42. package/skills/codelark-auto/SKILL.md +80 -0
  43. package/skills/codelark-question/SKILL.md +54 -0
@@ -0,0 +1,21 @@
1
+ export function readPackageJson(packageJsonUrl: string | URL): Promise<{ dependencies?: Record<string, string> }>;
2
+
3
+ export function hasInstalledPackage(
4
+ dependencyName: string,
5
+ options?: {
6
+ accessFile?: (path: string) => Promise<void>;
7
+ resolvePaths?: (dependencyName: string) => string[] | null;
8
+ },
9
+ ): Promise<boolean>;
10
+
11
+ export function findMissingRuntimeDependencies(
12
+ dependencies: Record<string, string> | undefined,
13
+ options?: {
14
+ accessFile?: (path: string) => Promise<void>;
15
+ resolvePaths?: (dependencyName: string) => string[] | null;
16
+ },
17
+ ): Promise<string[]>;
18
+
19
+ export function findMissingPackageJsonRuntimeDependencies(packageJsonUrl: string | URL): Promise<string[]>;
20
+
21
+ export function formatMissingRuntimeDependenciesMessage(missingDependencies: string[]): string;
@@ -0,0 +1,70 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import path from 'node:path';
4
+
5
+ const missingPackagePathErrorCodes = new Set(['ENOENT', 'ENOTDIR']);
6
+
7
+ export async function readPackageJson(packageJsonUrl) {
8
+ const raw = await readFile(packageJsonUrl, 'utf8');
9
+ return JSON.parse(raw);
10
+ }
11
+
12
+ export async function hasInstalledPackage(dependencyName, options = {}) {
13
+ const accessFile = options.accessFile ?? access;
14
+ const resolvePaths = options.resolvePaths ?? (() => []);
15
+
16
+ for (const nodeModulesPath of resolvePaths(dependencyName) ?? []) {
17
+ const packageJsonPath = path.join(nodeModulesPath, dependencyName, 'package.json');
18
+
19
+ try {
20
+ await accessFile(packageJsonPath);
21
+ return true;
22
+ } catch (error) {
23
+ if (!missingPackagePathErrorCodes.has(error?.code)) {
24
+ throw error;
25
+ }
26
+ }
27
+ }
28
+
29
+ return false;
30
+ }
31
+
32
+ export async function findMissingRuntimeDependencies(dependencies, options = {}) {
33
+ const dependencyNames = Object.keys(dependencies ?? {}).sort();
34
+ const missing = [];
35
+
36
+ for (const dependencyName of dependencyNames) {
37
+ if (!(await hasInstalledPackage(dependencyName, options))) {
38
+ missing.push(dependencyName);
39
+ }
40
+ }
41
+
42
+ return missing;
43
+ }
44
+
45
+ export async function findMissingPackageJsonRuntimeDependencies(packageJsonUrl) {
46
+ const packageJson = await readPackageJson(packageJsonUrl);
47
+ const packageRequire = createRequire(packageJsonUrl);
48
+
49
+ return findMissingRuntimeDependencies(packageJson.dependencies, {
50
+ resolvePaths: (dependencyName) => packageRequire.resolve.paths(dependencyName),
51
+ });
52
+ }
53
+
54
+ export function formatMissingRuntimeDependenciesMessage(missingDependencies) {
55
+ const dependencyList = missingDependencies.map((dependencyName) => ` - ${dependencyName}`).join('\n');
56
+
57
+ return [
58
+ 'CodeLark build cannot start because package.json runtime dependencies are not installed:',
59
+ dependencyList,
60
+ '',
61
+ 'Install dependencies first:',
62
+ ' npm ci',
63
+ '',
64
+ 'If you are updating an existing checkout, npm install is also acceptable:',
65
+ ' npm install',
66
+ '',
67
+ 'Then rerun:',
68
+ ' npm run build',
69
+ ].join('\n');
70
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ findMissingPackageJsonRuntimeDependencies,
3
+ formatMissingRuntimeDependenciesMessage,
4
+ } from './build-preflight.js';
5
+
6
+ const nodeMajor = Number((process.versions.node || '0').split('.')[0]);
7
+ if (!Number.isFinite(nodeMajor) || nodeMajor < 24) {
8
+ console.error(`CodeLark build requires Node.js 24 or newer. Current Node.js: ${process.version}.`);
9
+ process.exit(1);
10
+ }
11
+
12
+ const packageJsonUrl = new URL('../package.json', import.meta.url);
13
+ const missingRuntimeDependencies = await findMissingPackageJsonRuntimeDependencies(packageJsonUrl);
14
+ if (missingRuntimeDependencies.length > 0) {
15
+ console.error(formatMissingRuntimeDependenciesMessage(missingRuntimeDependencies));
16
+ process.exit(1);
17
+ }
18
+
19
+ const esbuild = await import('esbuild');
20
+
21
+ const common = {
22
+ bundle: true,
23
+ platform: 'node',
24
+ format: 'esm',
25
+ target: 'node24',
26
+ external: [
27
+ '@openai/codex-sdk',
28
+ // Keep large IM SDKs external so global/local npm installs resolve them
29
+ // from node_modules instead of inflating daemon.mjs.
30
+ '@larksuiteoapi/node-sdk',
31
+ // ws optional native deps
32
+ 'bufferutil', 'utf-8-validate',
33
+ // Node.js built-ins
34
+ 'fs', 'path', 'os', 'crypto', 'http', 'https', 'net', 'tls',
35
+ 'stream', 'events', 'url', 'util', 'child_process', 'worker_threads',
36
+ 'node:*',
37
+ ],
38
+ banner: { js: "import { createRequire as __codelarkCreateRequire } from 'module'; const require = __codelarkCreateRequire(import.meta.url);" },
39
+ };
40
+
41
+ async function build(entryPoint, outfile) {
42
+ await esbuild.build({
43
+ ...common,
44
+ entryPoints: [entryPoint],
45
+ outfile,
46
+ });
47
+ }
48
+
49
+ await build('src/entrypoints/daemon.ts', 'dist/daemon.mjs');
50
+ await build('src/operator-ui/server.ts', 'dist/ui-server.mjs');
51
+ await build('src/entrypoints/cli.ts', 'dist/cli.mjs');
52
+
53
+ console.log('Built dist/daemon.mjs, dist/ui-server.mjs, dist/cli.mjs');
@@ -0,0 +1,46 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ const forbiddenPrefixes = [
4
+ 'package/docs/',
5
+ 'package/docs/.vitepress/',
6
+ ];
7
+
8
+ const forbiddenFiles = new Set([
9
+ 'package/README_EN.md',
10
+ 'package/config.env.example',
11
+ ]);
12
+
13
+ const result = spawnSync('npm', ['pack', '--dry-run', '--json'], {
14
+ cwd: process.cwd(),
15
+ encoding: 'utf8',
16
+ stdio: ['ignore', 'pipe', 'pipe'],
17
+ });
18
+
19
+ if (result.status !== 0) {
20
+ process.stderr.write(result.stderr);
21
+ process.exit(result.status ?? 1);
22
+ }
23
+
24
+ let packEntries;
25
+ try {
26
+ packEntries = JSON.parse(result.stdout);
27
+ } catch (error) {
28
+ console.error('Unable to parse npm pack --dry-run --json output.');
29
+ console.error(error instanceof Error ? error.message : String(error));
30
+ process.exit(1);
31
+ }
32
+
33
+ const files = packEntries.flatMap((entry) => entry.files?.map((file) => file.path) ?? []);
34
+ const forbiddenMatches = files.filter((file) =>
35
+ forbiddenFiles.has(file) || forbiddenPrefixes.some((prefix) => file.startsWith(prefix)),
36
+ );
37
+
38
+ if (forbiddenMatches.length > 0) {
39
+ console.error('Unexpected files would be included in the npm package:');
40
+ for (const file of forbiddenMatches) {
41
+ console.error(`- ${file}`);
42
+ }
43
+ process.exit(1);
44
+ }
45
+
46
+ console.log(`npm package dry-run passed: ${files.length} files, no docs or example env files included.`);
@@ -0,0 +1,16 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Windows entry point — delegates to supervisor-windows.ps1.
4
+ .DESCRIPTION
5
+ Usage: powershell -File scripts\daemon.ps1 start|stop|status|logs|install-service|uninstall-service
6
+ #>
7
+ param(
8
+ [Parameter(Position=0)]
9
+ [string]$Command = 'help',
10
+
11
+ [Parameter(Position=1)]
12
+ [int]$LogLines = 50
13
+ )
14
+
15
+ $supervisorScript = Join-Path (Split-Path -Parent $PSCommandPath) 'supervisor-windows.ps1'
16
+ & $supervisorScript $Command $LogLines
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ CODELARK_HOME="${CODELARK_HOME:-$HOME/.codelark}"
4
+ SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+ PID_FILE="$CODELARK_HOME/runtime/bridge.pid"
6
+ STATUS_FILE="$CODELARK_HOME/runtime/status.json"
7
+ LOG_FILE="$CODELARK_HOME/logs/bridge.log"
8
+
9
+ # ── Common helpers ──
10
+
11
+ ensure_dirs() { mkdir -p "$CODELARK_HOME"/{data,logs,runtime,data/messages}; }
12
+
13
+ ensure_built() {
14
+ local need_build=0
15
+ if [ ! -f "$SKILL_DIR/dist/daemon.mjs" ]; then
16
+ need_build=1
17
+ else
18
+ # Check if any source file is newer than the bundle
19
+ local newest_src
20
+ newest_src=$(find "$SKILL_DIR/src" -name '*.ts' -newer "$SKILL_DIR/dist/daemon.mjs" 2>/dev/null | head -1)
21
+ if [ -n "$newest_src" ]; then
22
+ need_build=1
23
+ fi
24
+ # Also check if node_modules/codelark was updated (npm update)
25
+ # — its code is bundled into dist, so changes require a rebuild
26
+ if [ "$need_build" = "0" ] && [ -d "$SKILL_DIR/node_modules/codelark/src" ]; then
27
+ local newest_dep
28
+ newest_dep=$(find "$SKILL_DIR/node_modules/codelark/src" -name '*.ts' -newer "$SKILL_DIR/dist/daemon.mjs" 2>/dev/null | head -1)
29
+ if [ -n "$newest_dep" ]; then
30
+ need_build=1
31
+ fi
32
+ fi
33
+ fi
34
+ if [ "$need_build" = "1" ]; then
35
+ echo "Building daemon bundle..."
36
+ (cd "$SKILL_DIR" && npm run build)
37
+ fi
38
+ }
39
+
40
+ # Clean environment for subprocess isolation.
41
+ clean_env() {
42
+ unset CLAUDECODE 2>/dev/null || true
43
+
44
+ local mode="${CODELARK_ENV_ISOLATION:-inherit}"
45
+ if [ "$mode" = "strict" ]; then
46
+ while IFS='=' read -r name _; do
47
+ case "$name" in ANTHROPIC_*) unset "$name" 2>/dev/null || true ;; esac
48
+ done < <(env)
49
+ fi
50
+ }
51
+
52
+ read_pid() {
53
+ [ -f "$PID_FILE" ] && cat "$PID_FILE" 2>/dev/null || echo ""
54
+ }
55
+
56
+ pid_alive() {
57
+ local pid="$1"
58
+ [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
59
+ }
60
+
61
+ status_running() {
62
+ [ -f "$STATUS_FILE" ] && grep -q '"running"[[:space:]]*:[[:space:]]*true' "$STATUS_FILE" 2>/dev/null
63
+ }
64
+
65
+ show_last_exit_reason() {
66
+ if [ -f "$STATUS_FILE" ]; then
67
+ local reason
68
+ reason=$(grep -o '"lastExitReason"[[:space:]]*:[[:space:]]*"[^"]*"' "$STATUS_FILE" 2>/dev/null | head -1 | sed 's/.*: *"//;s/"$//')
69
+ [ -n "$reason" ] && echo "Last exit reason: $reason"
70
+ fi
71
+ }
72
+
73
+ show_failure_help() {
74
+ echo ""
75
+ echo "Recent logs:"
76
+ tail -20 "$LOG_FILE" 2>/dev/null || echo " (no log file)"
77
+ echo ""
78
+ echo "Next steps:"
79
+ echo " 1. Run diagnostics: bash \"$SKILL_DIR/scripts/doctor.sh\""
80
+ echo " 2. Check full logs: bash \"$SKILL_DIR/scripts/daemon.sh\" logs 100"
81
+ echo " 3. Rebuild bundle: cd \"$SKILL_DIR\" && npm run build"
82
+ }
83
+
84
+ # ── Load platform-specific supervisor ──
85
+
86
+ case "$(uname -s)" in
87
+ Darwin)
88
+ # shellcheck source=supervisor-macos.sh
89
+ source "$SKILL_DIR/scripts/supervisor-macos.sh"
90
+ ;;
91
+ MINGW*|MSYS*|CYGWIN*)
92
+ # Windows detected via Git Bash / MSYS2 / Cygwin — delegate to PowerShell
93
+ echo "Windows detected. Delegating to supervisor-windows.ps1..."
94
+ powershell.exe -ExecutionPolicy Bypass -File "$SKILL_DIR/scripts/supervisor-windows.ps1" "$@"
95
+ exit $?
96
+ ;;
97
+ *)
98
+ # shellcheck source=supervisor-linux.sh
99
+ source "$SKILL_DIR/scripts/supervisor-linux.sh"
100
+ ;;
101
+ esac
102
+
103
+ # ── Commands ──
104
+
105
+ case "${1:-help}" in
106
+ start)
107
+ ensure_dirs
108
+ ensure_built
109
+
110
+ # Check if already running (supervisor-aware: launchctl on macOS, PID on Linux)
111
+ if supervisor_is_running; then
112
+ EXISTING_PID=$(read_pid)
113
+ echo "Bridge already running${EXISTING_PID:+ (PID: $EXISTING_PID)}"
114
+ cat "$STATUS_FILE" 2>/dev/null
115
+ exit 1
116
+ fi
117
+
118
+ # Source config.env BEFORE clean_env so CODELARK_* flags are available.
119
+ [ -f "$CODELARK_HOME/config.env" ] && set -a && source "$CODELARK_HOME/config.env" && set +a
120
+
121
+ clean_env
122
+ echo "Starting bridge..."
123
+ supervisor_start
124
+
125
+ # Poll for up to 10 seconds waiting for status.json to report running
126
+ STARTED=false
127
+ for _ in $(seq 1 10); do
128
+ sleep 1
129
+ if status_running; then
130
+ STARTED=true
131
+ break
132
+ fi
133
+ # If supervisor process already died, stop waiting
134
+ if ! supervisor_is_running; then
135
+ break
136
+ fi
137
+ done
138
+
139
+ if [ "$STARTED" = "true" ]; then
140
+ NEW_PID=$(read_pid)
141
+ echo "Bridge started${NEW_PID:+ (PID: $NEW_PID)}"
142
+ cat "$STATUS_FILE" 2>/dev/null
143
+ else
144
+ echo "Failed to start bridge."
145
+ supervisor_is_running || echo " Process not running."
146
+ status_running || echo " status.json not reporting running=true."
147
+ show_last_exit_reason
148
+ show_failure_help
149
+ exit 1
150
+ fi
151
+ ;;
152
+
153
+ stop)
154
+ if supervisor_is_managed; then
155
+ echo "Stopping bridge..."
156
+ supervisor_stop
157
+ echo "Bridge stopped"
158
+ else
159
+ PID=$(read_pid)
160
+ if [ -z "$PID" ]; then echo "No bridge running"; exit 0; fi
161
+ if pid_alive "$PID"; then
162
+ kill "$PID"
163
+ for _ in $(seq 1 10); do
164
+ pid_alive "$PID" || break
165
+ sleep 1
166
+ done
167
+ pid_alive "$PID" && kill -9 "$PID"
168
+ echo "Bridge stopped"
169
+ else
170
+ echo "Bridge was not running (stale PID file)"
171
+ fi
172
+ rm -f "$PID_FILE"
173
+ fi
174
+ ;;
175
+
176
+ status)
177
+ # Platform-specific status info (prints launchd/service state)
178
+ supervisor_status_extra
179
+
180
+ # Process status: supervisor-aware (launchctl on macOS, PID on Linux)
181
+ if supervisor_is_running; then
182
+ PID=$(read_pid)
183
+ echo "Bridge process is running${PID:+ (PID: $PID)}"
184
+ # Business status from status.json
185
+ if status_running; then
186
+ echo "Bridge status: running"
187
+ else
188
+ echo "Bridge status: process alive but status.json not reporting running"
189
+ fi
190
+ cat "$STATUS_FILE" 2>/dev/null
191
+ else
192
+ echo "Bridge is not running"
193
+ [ -f "$PID_FILE" ] && rm -f "$PID_FILE"
194
+ show_last_exit_reason
195
+ fi
196
+ ;;
197
+
198
+ logs)
199
+ N="${2:-50}"
200
+ tail -n "$N" "$LOG_FILE" 2>/dev/null | sed -E 's/(token|secret|password)(["\\x27]?\s*[:=]\s*["\\x27]?)[^ "]+/\1\2*****/gi'
201
+ ;;
202
+
203
+ *)
204
+ echo "Usage: daemon.sh {start|stop|status|logs [N]}"
205
+ ;;
206
+ esac
@@ -0,0 +1,27 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Windows wrapper for the existing bash-based doctor script.
4
+ .DESCRIPTION
5
+ Prefers Git Bash / bash.exe when available. Falls back to a clear message
6
+ when bash is missing, because the full diagnostics live in doctor.sh.
7
+ #>
8
+
9
+ $ErrorActionPreference = 'Stop'
10
+
11
+ $doctorScript = Join-Path (Split-Path -Parent $PSCommandPath) 'doctor.sh'
12
+
13
+ if (-not (Test-Path $doctorScript)) {
14
+ Write-Error "doctor.sh not found at $doctorScript"
15
+ exit 1
16
+ }
17
+
18
+ $bash = Get-Command bash -ErrorAction SilentlyContinue
19
+ if (-not $bash) {
20
+ Write-Host "bash was not found in PATH."
21
+ Write-Host "Install Git Bash or another bash environment, then run:"
22
+ Write-Host " bash `"$doctorScript`""
23
+ exit 1
24
+ }
25
+
26
+ & $bash.Source $doctorScript
27
+ exit $LASTEXITCODE
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ CODELARK_HOME="${CODELARK_HOME:-${CODELARK_HOME:-$HOME/.codelark}}"
4
+ CONFIG_FILE="$CODELARK_HOME/config.env"
5
+ PID_FILE="$CODELARK_HOME/runtime/bridge.pid"
6
+ LOG_FILE="$CODELARK_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
+ CODELARK_RUNTIME=$(get_config CODELARK_RUNTIME)
41
+ CODELARK_RUNTIME="codex"
42
+ echo "Runtime: $CODELARK_RUNTIME"
43
+ echo ""
44
+
45
+ # --- Codex checks ---
46
+ if command -v codex &>/dev/null; then
47
+ CODEX_VER=$(codex --version 2>/dev/null || echo "unknown")
48
+ check "Codex CLI available (${CODEX_VER})" 0
49
+ else
50
+ check "Codex CLI available (not found in PATH)" 1
51
+ fi
52
+
53
+ # Check @openai/codex-sdk
54
+ CODEX_SDK="$SKILL_DIR/node_modules/@openai/codex-sdk"
55
+ if [ -d "$CODEX_SDK" ]; then
56
+ check "@openai/codex-sdk installed" 0
57
+ else
58
+ check "@openai/codex-sdk installed (not found — run 'npm install' in $SKILL_DIR)" 1
59
+ fi
60
+
61
+ # Check Codex auth: any of CODELARK_CODEX_API_KEY / CODEX_API_KEY / OPENAI_API_KEY,
62
+ # or `codex auth status` showing logged-in (interactive login).
63
+ CODEX_AUTH=1
64
+ if [ -n "${CODELARK_CODEX_API_KEY:-}" ] || [ -n "${CODEX_API_KEY:-}" ] || [ -n "${OPENAI_API_KEY:-}" ]; then
65
+ CODEX_AUTH=0
66
+ elif command -v codex &>/dev/null; then
67
+ CODEX_AUTH_OUT=$(codex auth status 2>&1 || true)
68
+ if echo "$CODEX_AUTH_OUT" | grep -qiE 'logged.in|authenticated'; then
69
+ CODEX_AUTH=0
70
+ fi
71
+ fi
72
+ if [ "$CODEX_AUTH" = "0" ]; then
73
+ check "Codex auth available (API key or login)" 0
74
+ else
75
+ check "Codex auth available (set OPENAI_API_KEY or run 'codex auth login')" 1
76
+ fi
77
+
78
+ # --- dist/daemon.mjs freshness ---
79
+ DAEMON_MJS="$SKILL_DIR/dist/daemon.mjs"
80
+ if [ -f "$DAEMON_MJS" ]; then
81
+ STALE_SRC=$(find "$SKILL_DIR/src" -name '*.ts' -newer "$DAEMON_MJS" 2>/dev/null | head -1)
82
+ if [ -z "$STALE_SRC" ]; then
83
+ check "dist/daemon.mjs is up to date" 0
84
+ else
85
+ check "dist/daemon.mjs is stale (src changed, run 'npm run build')" 1
86
+ fi
87
+ else
88
+ check "dist/daemon.mjs exists (not built — run 'npm run build')" 1
89
+ fi
90
+
91
+ # --- config.env exists ---
92
+ if [ -f "$CONFIG_FILE" ]; then
93
+ check "config.env exists" 0
94
+ else
95
+ check "config.env exists ($CONFIG_FILE not found)" 1
96
+ fi
97
+
98
+ # --- config.env permissions ---
99
+ if [ -f "$CONFIG_FILE" ]; then
100
+ PERMS=$(stat -f "%Lp" "$CONFIG_FILE" 2>/dev/null || stat -c "%a" "$CONFIG_FILE" 2>/dev/null || echo "unknown")
101
+ if [ "$PERMS" = "600" ]; then
102
+ check "config.env permissions are 600" 0
103
+ else
104
+ check "config.env permissions are 600 (currently $PERMS)" 1
105
+ fi
106
+ fi
107
+
108
+ # --- Load config for channel checks ---
109
+ if [ -f "$CONFIG_FILE" ]; then
110
+ CODELARK_CHANNELS=$(get_config CODELARK_ENABLED_CHANNELS)
111
+
112
+ # --- Feishu ---
113
+ if echo "$CODELARK_CHANNELS" | grep -q feishu; then
114
+ FS_APP_ID=$(get_config CODELARK_FEISHU_APP_ID)
115
+ FS_SECRET=$(get_config CODELARK_FEISHU_APP_SECRET)
116
+ FS_SITE=$(get_config CODELARK_FEISHU_SITE)
117
+ case "$FS_SITE" in
118
+ lark|*open.larksuite.com*)
119
+ FS_DOMAIN="https://open.larksuite.com"
120
+ ;;
121
+ *)
122
+ FS_DOMAIN="https://open.feishu.cn"
123
+ ;;
124
+ esac
125
+ if [ -n "$FS_APP_ID" ] && [ -n "$FS_SECRET" ]; then
126
+ FEISHU_RESULT=$(curl -s --max-time 5 -X POST "${FS_DOMAIN}/open-apis/auth/v3/tenant_access_token/internal" \
127
+ -H "Content-Type: application/json" \
128
+ -d "{\"app_id\":\"${FS_APP_ID}\",\"app_secret\":\"${FS_SECRET}\"}" 2>/dev/null || echo '{"code":1}')
129
+ if echo "$FEISHU_RESULT" | grep -q '"code"[[:space:]]*:[[:space:]]*0'; then
130
+ check "Feishu app credentials are valid" 0
131
+ else
132
+ check "Feishu app credentials are valid (token request failed)" 1
133
+ fi
134
+ else
135
+ check "Feishu app credentials configured" 1
136
+ fi
137
+ fi
138
+
139
+ fi
140
+
141
+ # --- Log directory writable ---
142
+ LOG_DIR="$CODELARK_HOME/logs"
143
+ if [ -d "$LOG_DIR" ] && [ -w "$LOG_DIR" ]; then
144
+ check "Log directory is writable" 0
145
+ else
146
+ check "Log directory is writable ($LOG_DIR)" 1
147
+ fi
148
+
149
+ # --- PID file consistency ---
150
+ if [ -f "$PID_FILE" ]; then
151
+ PID=$(cat "$PID_FILE")
152
+ if kill -0 "$PID" 2>/dev/null; then
153
+ check "PID file consistent (process $PID is running)" 0
154
+ else
155
+ check "PID file consistent (stale PID $PID, process not running)" 1
156
+ fi
157
+ else
158
+ check "PID file consistency (no PID file, OK)" 0
159
+ fi
160
+
161
+ # --- Recent errors in log ---
162
+ if [ -f "$LOG_FILE" ]; then
163
+ ERROR_COUNT=$(tail -50 "$LOG_FILE" | grep -ciE 'ERROR|Fatal' || true)
164
+ if [ "$ERROR_COUNT" -eq 0 ]; then
165
+ check "No recent errors in log (last 50 lines)" 0
166
+ else
167
+ check "No recent errors in log (found $ERROR_COUNT ERROR/Fatal lines)" 1
168
+ fi
169
+ else
170
+ check "Log file exists (not yet created)" 0
171
+ fi
172
+
173
+ echo ""
174
+ echo "Results: $PASS passed, $FAIL failed"
175
+
176
+ if [ "$FAIL" -gt 0 ]; then
177
+ echo ""
178
+ echo "Common fixes:"
179
+ echo " SDK cli.js missing → cd $SKILL_DIR && npm install"
180
+ echo " dist/daemon.mjs stale → cd $SKILL_DIR && npm run build"
181
+ echo " config.env missing → run setup wizard"
182
+ echo " Stale PID file → run stop, then start"
183
+ fi
184
+
185
+ [ "$FAIL" -eq 0 ] && exit 0 || exit 1