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,83 @@
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(), 'codelark-test-'));
7
+ const runtimeHome = path.join(tempHome, 'runtime-home');
8
+ const codexHome = path.join(tempHome, 'codex-home');
9
+ const claudeHome = path.join(tempHome, 'claude-home');
10
+ fs.mkdirSync(runtimeHome, { recursive: true });
11
+ fs.mkdirSync(codexHome, { recursive: true });
12
+ fs.mkdirSync(claudeHome, { recursive: true });
13
+
14
+ const testsDir = path.join(process.cwd(), 'src', '__tests__');
15
+
16
+ function discoverTestFiles(dir) {
17
+ return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
18
+ const fullPath = path.join(dir, entry.name);
19
+ if (entry.isDirectory()) {
20
+ return discoverTestFiles(fullPath);
21
+ }
22
+ if (!entry.isFile() || !entry.name.endsWith('.test.ts')) {
23
+ return [];
24
+ }
25
+ return [path.relative(process.cwd(), fullPath)];
26
+ });
27
+ }
28
+
29
+ const layerFilters = new Map([
30
+ ['--unit', path.join('src', '__tests__', 'unit') + path.sep],
31
+ ['--workflow', path.join('src', '__tests__', 'workflow') + path.sep],
32
+ ['--mock-e2e', path.join('src', '__tests__', 'e2e', 'mock-app') + path.sep],
33
+ ['--local-e2e', path.join('src', '__tests__', 'e2e', 'local-process') + path.sep],
34
+ ['--harness', path.join('src', '__tests__', 'harness') + path.sep],
35
+ ]);
36
+
37
+ const requestedLayers = process.argv.slice(2).filter((arg) => layerFilters.has(arg));
38
+ const testFiles = discoverTestFiles(testsDir)
39
+ .sort()
40
+ .filter((file) => requestedLayers.length === 0 || requestedLayers.some((arg) => file.startsWith(layerFilters.get(arg))));
41
+
42
+ if (testFiles.length === 0) {
43
+ console.error(`No test files matched ${requestedLayers.join(', ') || 'the current test discovery pattern'}.`);
44
+ process.exit(1);
45
+ }
46
+
47
+ const child = spawn(
48
+ process.execPath,
49
+ [
50
+ '--test',
51
+ '--test-concurrency=1',
52
+ '--import',
53
+ 'tsx',
54
+ '--test-timeout=15000',
55
+ ...testFiles,
56
+ ],
57
+ {
58
+ stdio: 'inherit',
59
+ env: {
60
+ ...process.env,
61
+ HOME: runtimeHome,
62
+ USERPROFILE: runtimeHome,
63
+ CODELARK_HOME: tempHome,
64
+ CODEX_HOME: codexHome,
65
+ CODELARK_CLAUDE_HOME: claudeHome,
66
+ },
67
+ },
68
+ );
69
+
70
+ child.on('exit', (code, signal) => {
71
+ try {
72
+ fs.rmSync(tempHome, { recursive: true, force: true });
73
+ } catch {
74
+ // ignore
75
+ }
76
+
77
+ if (signal) {
78
+ process.kill(process.pid, signal);
79
+ return;
80
+ }
81
+
82
+ process.exit(code ?? 1);
83
+ });
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ type FeishuSite = 'feishu' | 'lark';
7
+
8
+ function valueArg(args: string[], name: string, fallback = ''): string {
9
+ const index = args.indexOf(name);
10
+ if (index < 0) return fallback;
11
+ return args[index + 1] || fallback;
12
+ }
13
+
14
+ function hasFlag(args: string[], name: string): boolean {
15
+ return args.includes(name);
16
+ }
17
+
18
+ function printUsage(): void {
19
+ process.stdout.write([
20
+ 'Usage:',
21
+ ' CODELARK_SETUP_WIZARD_REAL_E2E=1 npm run real:setup-wizard:e2e -- [options]',
22
+ '',
23
+ 'Options:',
24
+ ' --run-root <path> Temporary root; default /tmp/clk-setup-wizard-real-e2e-<timestamp>',
25
+ ' --test-env-file <path> Load test app credentials from an env file; avoids npm argument echo',
26
+ ' --app-id <cli_...> App ID written to isolated lark-cli/CodeLark config',
27
+ ' --app-secret <secret> App Secret for the isolated smoke app; prefer env file/env vars to avoid npm echo',
28
+ ' --site <feishu|lark> Site brand; default feishu',
29
+ ' --keep-temp Keep temporary root for diagnosis; default cleans it in success and failure paths',
30
+ ' --simulate-failure-after-sync Test cleanup on a post-lark-cli failure',
31
+ ' --help Show this help',
32
+ '',
33
+ ].join('\n'));
34
+ }
35
+
36
+ function parseEnvFile(filePath: string): Record<string, string> {
37
+ if (!filePath) return {};
38
+ const content = fs.readFileSync(filePath, 'utf-8');
39
+ const values: Record<string, string> = {};
40
+ for (const rawLine of content.split(/\r?\n/)) {
41
+ const line = rawLine.trim();
42
+ if (!line || line.startsWith('#')) continue;
43
+ const separator = line.indexOf('=');
44
+ if (separator <= 0) continue;
45
+ const key = line.slice(0, separator).trim();
46
+ let value = line.slice(separator + 1).trim();
47
+ if (
48
+ (value.startsWith('"') && value.endsWith('"'))
49
+ || (value.startsWith("'") && value.endsWith("'"))
50
+ ) {
51
+ value = value.slice(1, -1);
52
+ }
53
+ values[key] = value;
54
+ }
55
+ return values;
56
+ }
57
+
58
+ function envValue(envFile: Record<string, string>, key: string, legacyKey?: string): string {
59
+ return process.env[key] || envFile[key] || (legacyKey ? process.env[legacyKey] || envFile[legacyKey] || '' : '');
60
+ }
61
+
62
+ function assertInside(parentPath: string, childPath: string): void {
63
+ const parent = path.resolve(parentPath);
64
+ const child = path.resolve(childPath);
65
+ const relative = path.relative(parent, child);
66
+ if (relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative))) return;
67
+ throw new Error(`Path escaped temp root: ${child}`);
68
+ }
69
+
70
+ async function main(): Promise<void> {
71
+ const argv = process.argv.slice(2);
72
+ if (hasFlag(argv, '--help')) {
73
+ printUsage();
74
+ return;
75
+ }
76
+ if (process.env.CODELARK_SETUP_WIZARD_REAL_E2E !== '1') {
77
+ throw new Error('Refusing to run setup wizard real e2e without CODELARK_SETUP_WIZARD_REAL_E2E=1.');
78
+ }
79
+ const testEnvFile = valueArg(argv, '--test-env-file', '');
80
+ const envFile = parseEnvFile(testEnvFile);
81
+
82
+ const runRoot = path.resolve(valueArg(
83
+ argv,
84
+ '--run-root',
85
+ path.join(os.tmpdir(), `clk-setup-wizard-real-e2e-${Date.now()}`),
86
+ ));
87
+ const keepTemp = hasFlag(argv, '--keep-temp');
88
+ const appId = valueArg(
89
+ argv,
90
+ '--app-id',
91
+ envValue(envFile, 'CODELARK_REAL_FEISHU_TEST_APP_ID', 'CTI_REAL_FEISHU_TEST_APP_ID') || 'cli_setup_wizard_real_e2e',
92
+ );
93
+ const appSecret = valueArg(
94
+ argv,
95
+ '--app-secret',
96
+ envValue(envFile, 'CODELARK_REAL_FEISHU_TEST_APP_SECRET', 'CTI_REAL_FEISHU_TEST_APP_SECRET') || 'setup-wizard-real-e2e-secret',
97
+ );
98
+ const siteArg = valueArg(
99
+ argv,
100
+ '--site',
101
+ envValue(envFile, 'CODELARK_REAL_FEISHU_TEST_SITE', 'CTI_REAL_FEISHU_TEST_SITE') || 'feishu',
102
+ );
103
+ const site: FeishuSite = siteArg === 'lark' ? 'lark' : 'feishu';
104
+
105
+ const runtimeHome = path.join(runRoot, 'home');
106
+ const codelarkHome = path.join(runRoot, 'clk-home');
107
+ const workspaceRoot = path.join(runRoot, 'workspace');
108
+ const larkConfigPath = path.join(runtimeHome, '.lark-cli', 'config.json');
109
+ const configEnvPath = path.join(codelarkHome, 'config.env');
110
+ const configJsonPath = path.join(codelarkHome, 'config.json');
111
+
112
+ try {
113
+ assertInside(os.tmpdir(), runRoot);
114
+ fs.mkdirSync(runtimeHome, { recursive: true });
115
+ fs.mkdirSync(codelarkHome, { recursive: true });
116
+ fs.mkdirSync(workspaceRoot, { recursive: true });
117
+ fs.writeFileSync(configEnvPath, '# custom env survives setup\nCUSTOM_KEEP=1\n', { mode: 0o600 });
118
+
119
+ process.env.HOME = runtimeHome;
120
+ process.env.USERPROFILE = runtimeHome;
121
+ process.env.CODELARK_HOME = codelarkHome;
122
+
123
+ const configuration = await import('../src/configuration/index.js');
124
+ const setupWizard = await import('../src/entrypoints/setup-wizard.js');
125
+
126
+ const credentials = {
127
+ appId,
128
+ appSecret,
129
+ site,
130
+ alias: 'codelark',
131
+ };
132
+
133
+ await setupWizard.syncLarkCliCredentials(credentials, runtimeHome);
134
+ const entriesAfterSync = setupWizard.readLarkCliAppEntries(larkConfigPath);
135
+ const syncedEntry = entriesAfterSync.find((entry) => entry.appId === appId);
136
+ if (!syncedEntry) throw new Error(`lark-cli config missing synced app ${appId}`);
137
+ if (syncedEntry.brand !== site) throw new Error(`lark-cli brand mismatch: ${syncedEntry.brand}`);
138
+ if (syncedEntry.appSecret) {
139
+ throw new Error('real lark-cli e2e expected lark-cli to avoid plaintext appSecret in this environment');
140
+ }
141
+ if (syncedEntry.secretStorage !== 'keychain') {
142
+ throw new Error(`expected keychain/local encrypted lark-cli secret storage, got ${syncedEntry.secretStorage}`);
143
+ }
144
+ const importableApps = setupWizard.readLarkCliApps(larkConfigPath);
145
+ const importableApp = importableApps.find((entry) => entry.appId === appId);
146
+ if (!importableApp) throw new Error(`CodeLark could not import synced lark-cli app ${appId}`);
147
+ if (importableApp.appSecret !== appSecret) {
148
+ throw new Error('CodeLark did not recover App Secret from lark-cli local encrypted storage');
149
+ }
150
+ if (hasFlag(argv, '--simulate-failure-after-sync')) {
151
+ throw new Error('simulated setup wizard real e2e failure after lark-cli sync');
152
+ }
153
+
154
+ const current = configuration.loadConfig();
155
+ configuration.saveConfig(setupWizard.buildSetupConfig(current, credentials, 'codex', workspaceRoot));
156
+
157
+ const savedConfig = configuration.loadConfig();
158
+ const savedFeishu = savedConfig.channels?.find((channel) => channel.provider === 'feishu');
159
+ if (savedConfig.runtime !== 'codex') throw new Error(`runtime mismatch: ${savedConfig.runtime}`);
160
+ if (savedConfig.defaultWorkspaceRoot !== workspaceRoot) {
161
+ throw new Error(`workspace mismatch: ${savedConfig.defaultWorkspaceRoot}`);
162
+ }
163
+ if (savedFeishu?.config.appId !== appId) throw new Error('config.v1 appId mismatch');
164
+ if (savedFeishu?.config.appSecret !== appSecret) throw new Error('config.v1 appSecret mismatch');
165
+ if (savedFeishu?.config.site !== site) throw new Error('config.v1 site mismatch');
166
+
167
+ const envContent = fs.readFileSync(configEnvPath, 'utf-8');
168
+ if (!envContent.includes('CUSTOM_KEEP=1')) throw new Error('custom config.env line was not preserved');
169
+ if (!envContent.includes(`CODELARK_FEISHU_APP_ID=${appId}`)) throw new Error('config.env appId missing');
170
+ if (!envContent.includes(`CODELARK_FEISHU_APP_SECRET=${appSecret}`)) throw new Error('config.env appSecret missing');
171
+ if (!fs.existsSync(configJsonPath)) throw new Error('config.json missing');
172
+
173
+ const result = {
174
+ ok: true,
175
+ runRoot,
176
+ runtimeHome,
177
+ codelarkHome,
178
+ larkConfigPath,
179
+ configEnvPath,
180
+ configJsonPath,
181
+ larkSecretStorage: syncedEntry.secretStorage,
182
+ cleanedRunRoot: !keepTemp,
183
+ };
184
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
185
+ } finally {
186
+ if (!keepTemp) {
187
+ fs.rmSync(runRoot, { recursive: true, force: true });
188
+ }
189
+ }
190
+ }
191
+
192
+ main().catch((error) => {
193
+ process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
194
+ process.exit(1);
195
+ });
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bash
2
+ # Linux supervisor — setsid/nohup fallback process management.
3
+ # Sourced by daemon.sh; expects CODELARK_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
+ }
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env bash
2
+ # macOS supervisor — launchd-based process management.
3
+ # Sourced by daemon.sh; expects CODELARK_HOME, SKILL_DIR, PID_FILE, STATUS_FILE, LOG_FILE.
4
+
5
+ LAUNCHD_LABEL="com.codelark.bridge"
6
+ PLIST_DIR="$HOME/Library/LaunchAgents"
7
+ PLIST_FILE="$PLIST_DIR/$LAUNCHD_LABEL.plist"
8
+
9
+ # ── launchd helpers ──
10
+
11
+ launchd_target() {
12
+ local label="$1"
13
+ echo "gui/$(id -u)/$label"
14
+ }
15
+
16
+ launchd_bootout() {
17
+ local label="$1"
18
+ launchctl bootout "$(launchd_target "$label")" 2>/dev/null || true
19
+ }
20
+
21
+ launchd_print() {
22
+ local label="$1"
23
+ launchctl print "$(launchd_target "$label")" 2>/dev/null
24
+ }
25
+
26
+ launchd_active_label() {
27
+ if launchd_print "$LAUNCHD_LABEL" >/dev/null; then
28
+ echo "$LAUNCHD_LABEL"
29
+ return 0
30
+ fi
31
+ return 1
32
+ }
33
+
34
+ launchd_pid_for_label() {
35
+ local label="$1"
36
+ launchd_print "$label" | grep -m1 'pid = ' | sed 's/.*pid = //' | tr -d ' '
37
+ }
38
+
39
+ # Collect env vars that should be forwarded into the plist.
40
+ # We honour clean_env() logic by reading *after* clean_env runs.
41
+ build_env_dict() {
42
+ local indent=" "
43
+ local dict=""
44
+
45
+ # Always forward basics
46
+ for var in HOME PATH USER SHELL LANG TMPDIR; do
47
+ local val="${!var:-}"
48
+ [ -z "$val" ] && continue
49
+ dict+="${indent}<key>${var}</key>\n${indent}<string>${val}</string>\n"
50
+ done
51
+
52
+ # Forward CodeLark-specific vars.
53
+ while IFS='=' read -r name val; do
54
+ case "$name" in CODELARK_*)
55
+ dict+="${indent}<key>${name}</key>\n${indent}<string>${val}</string>\n"
56
+ ;; esac
57
+ done < <(env)
58
+
59
+ # Forward Codex/OpenAI credentials used by the Codex runtime.
60
+ for var in OPENAI_API_KEY CODEX_API_KEY CODELARK_CODEX_API_KEY CODELARK_CODEX_BASE_URL; do
61
+ local val="${!var:-}"
62
+ [ -z "$val" ] && continue
63
+ dict+="${indent}<key>${var}</key>\n${indent}<string>${val}</string>\n"
64
+ done
65
+
66
+ echo -e "$dict"
67
+ }
68
+
69
+ generate_plist() {
70
+ local node_path
71
+ node_path=$(command -v node)
72
+
73
+ mkdir -p "$PLIST_DIR"
74
+ local env_dict
75
+ env_dict=$(build_env_dict)
76
+
77
+ cat > "$PLIST_FILE" <<PLIST
78
+ <?xml version="1.0" encoding="UTF-8"?>
79
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
80
+ <plist version="1.0">
81
+ <dict>
82
+ <key>Label</key>
83
+ <string>${LAUNCHD_LABEL}</string>
84
+
85
+ <key>ProgramArguments</key>
86
+ <array>
87
+ <string>${node_path}</string>
88
+ <string>${SKILL_DIR}/dist/daemon.mjs</string>
89
+ </array>
90
+
91
+ <key>WorkingDirectory</key>
92
+ <string>${SKILL_DIR}</string>
93
+
94
+ <key>StandardOutPath</key>
95
+ <string>${LOG_FILE}</string>
96
+ <key>StandardErrorPath</key>
97
+ <string>${LOG_FILE}</string>
98
+
99
+ <key>RunAtLoad</key>
100
+ <false/>
101
+
102
+ <key>KeepAlive</key>
103
+ <dict>
104
+ <key>SuccessfulExit</key>
105
+ <false/>
106
+ </dict>
107
+
108
+ <key>ThrottleInterval</key>
109
+ <integer>10</integer>
110
+
111
+ <key>EnvironmentVariables</key>
112
+ <dict>
113
+ ${env_dict} </dict>
114
+ </dict>
115
+ </plist>
116
+ PLIST
117
+ }
118
+
119
+ # ── Public interface (called by daemon.sh) ──
120
+
121
+ supervisor_start() {
122
+ launchd_bootout "$LAUNCHD_LABEL"
123
+ generate_plist
124
+ launchctl bootstrap "gui/$(id -u)" "$PLIST_FILE"
125
+ launchctl kickstart -k "$(launchd_target "$LAUNCHD_LABEL")"
126
+ }
127
+
128
+ supervisor_stop() {
129
+ launchd_bootout "$LAUNCHD_LABEL"
130
+ rm -f "$PID_FILE"
131
+ }
132
+
133
+ supervisor_is_managed() {
134
+ launchd_active_label >/dev/null
135
+ }
136
+
137
+ supervisor_status_extra() {
138
+ local active_label
139
+ active_label=$(launchd_active_label 2>/dev/null || true)
140
+ if [ -n "$active_label" ]; then
141
+ echo "Bridge is registered with launchd ($active_label)"
142
+ # Extract PID from launchctl as the authoritative source
143
+ local lc_pid
144
+ lc_pid=$(launchd_pid_for_label "$active_label")
145
+ if [ -n "$lc_pid" ] && [ "$lc_pid" != "0" ] && [ "$lc_pid" != "-" ]; then
146
+ echo "launchd reports PID: $lc_pid"
147
+ fi
148
+ fi
149
+ }
150
+
151
+ # Override: on macOS, check launchctl first, then fall back to PID file
152
+ supervisor_is_running() {
153
+ # Primary: launchctl knows the process
154
+ local active_label
155
+ active_label=$(launchd_active_label 2>/dev/null || true)
156
+ if [ -n "$active_label" ]; then
157
+ local lc_pid
158
+ lc_pid=$(launchd_pid_for_label "$active_label")
159
+ if [ -n "$lc_pid" ] && [ "$lc_pid" != "0" ] && [ "$lc_pid" != "-" ]; then
160
+ return 0
161
+ fi
162
+ fi
163
+ # Fallback: PID file
164
+ local pid
165
+ pid=$(read_pid)
166
+ pid_alive "$pid"
167
+ }