feishu-codex-connector 0.1.6

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/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare function main(argv?: string[]): Promise<void>;
2
+
3
+ export { main };
package/dist/cli.js ADDED
@@ -0,0 +1,454 @@
1
+ import {
2
+ ProfileStore,
3
+ RuntimeRegistry,
4
+ profilePaths,
5
+ resolveWorkspace,
6
+ runBridge,
7
+ runnerForProfile
8
+ } from "./chunk-AOVT2UMM.js";
9
+
10
+ // src/cli.ts
11
+ import { Command } from "commander";
12
+ import { createRequire } from "module";
13
+
14
+ // src/daemon/service-adapter.ts
15
+ import { spawnSync } from "child_process";
16
+ import { existsSync } from "fs";
17
+ import { homedir, platform } from "os";
18
+ import { basename, join } from "path";
19
+ import { mkdir, rm, writeFile } from "fs/promises";
20
+ function serviceAdapter(home) {
21
+ const os = platform();
22
+ if (os === "darwin") return new LaunchdServiceAdapter(home);
23
+ if (os === "linux") return new SystemdServiceAdapter(home);
24
+ if (os === "win32") return new WindowsTaskServiceAdapter(home);
25
+ return new UnsupportedServiceAdapter(os);
26
+ }
27
+ var LaunchdServiceAdapter = class {
28
+ constructor(home) {
29
+ this.home = home;
30
+ }
31
+ home;
32
+ async install(profile, cliPath) {
33
+ const path = launchdPath(profile);
34
+ await mkdir(join(path, ".."), { recursive: true });
35
+ const logs = profilePaths(profile, this.home).logsDir;
36
+ await mkdir(logs, { recursive: true });
37
+ await writeFile(path, plist(profile, cliPath, logs), { mode: 420 });
38
+ }
39
+ async start(profile, cliPath) {
40
+ await stopLaunchdLabels([legacyLabel(profile)]);
41
+ await rm(launchdPathForLabel(legacyLabel(profile)), { force: true });
42
+ await this.install(profile, cliPath);
43
+ const result = run("launchctl", ["bootstrap", launchdDomain(), launchdPath(profile)]);
44
+ if (!result.ok && !/already bootstrapped|service already loaded/i.test(result.stderr)) throw new Error(result.stderr || "launchctl bootstrap failed");
45
+ const kick = run("launchctl", ["kickstart", "-k", `${launchdDomain()}/${label(profile)}`]);
46
+ if (!kick.ok) throw new Error(kick.stderr || "launchctl kickstart failed");
47
+ }
48
+ async stop(profile) {
49
+ await stopLaunchdLabels(serviceLabels(profile));
50
+ }
51
+ async restart(profile, cliPath) {
52
+ await stopLaunchdLabels([legacyLabel(profile)]);
53
+ await rm(launchdPathForLabel(legacyLabel(profile)), { force: true });
54
+ await this.install(profile, cliPath);
55
+ const result = run("launchctl", ["kickstart", "-k", `${launchdDomain()}/${label(profile)}`]);
56
+ if (!result.ok) {
57
+ await this.start(profile, cliPath);
58
+ }
59
+ }
60
+ async unregister(profile) {
61
+ await this.stop(profile).catch(() => void 0);
62
+ await Promise.all(serviceLabels(profile).map((serviceLabel) => rm(launchdPathForLabel(serviceLabel), { force: true })));
63
+ }
64
+ async status(profile) {
65
+ const states = serviceLabels(profile).map((serviceLabel) => {
66
+ const path = launchdPathForLabel(serviceLabel);
67
+ const result = run("launchctl", ["print", `${launchdDomain()}/${serviceLabel}`]);
68
+ return { serviceLabel, path, result };
69
+ });
70
+ const running = states.some((state) => state.result.ok);
71
+ const installed = states.some((state) => existsSync(state.path));
72
+ const visibleStates = states.filter((state) => state.result.ok || existsSync(state.path));
73
+ const detail = (visibleStates.length > 0 ? visibleStates : states).map((state) => `${state.serviceLabel}: ${state.result.stdout || state.result.stderr}`).join("\n");
74
+ return { installed, running, detail, servicePath: launchdPath(profile) };
75
+ }
76
+ };
77
+ var SystemdServiceAdapter = class {
78
+ constructor(home) {
79
+ this.home = home;
80
+ }
81
+ home;
82
+ async install(profile, cliPath) {
83
+ const path = systemdPath(profile);
84
+ await mkdir(join(path, ".."), { recursive: true });
85
+ const logs = profilePaths(profile, this.home).logsDir;
86
+ await mkdir(logs, { recursive: true });
87
+ await writeFile(path, systemdUnit(profile, cliPath, logs), { mode: 420 });
88
+ run("systemctl", ["--user", "daemon-reload"]);
89
+ }
90
+ async start(profile, cliPath) {
91
+ stopSystemdUnits([legacySystemdUnitName(profile)]);
92
+ await rm(systemdPathForUnit(legacySystemdUnitName(profile)), { force: true });
93
+ run("systemctl", ["--user", "daemon-reload"]);
94
+ await this.install(profile, cliPath);
95
+ const result = run("systemctl", ["--user", "enable", "--now", systemdUnitName(profile)]);
96
+ if (!result.ok) throw new Error(result.stderr || "systemctl enable --now failed");
97
+ }
98
+ async stop(profile) {
99
+ stopSystemdUnits(serviceUnitNames(profile));
100
+ }
101
+ async restart(profile, cliPath) {
102
+ stopSystemdUnits([legacySystemdUnitName(profile)]);
103
+ await rm(systemdPathForUnit(legacySystemdUnitName(profile)), { force: true });
104
+ run("systemctl", ["--user", "daemon-reload"]);
105
+ await this.install(profile, cliPath);
106
+ const result = run("systemctl", ["--user", "restart", systemdUnitName(profile)]);
107
+ if (!result.ok) throw new Error(result.stderr || "systemctl restart failed");
108
+ }
109
+ async unregister(profile) {
110
+ for (const unit of serviceUnitNames(profile)) run("systemctl", ["--user", "disable", "--now", unit]);
111
+ await Promise.all(serviceUnitNames(profile).map((unit) => rm(systemdPathForUnit(unit), { force: true })));
112
+ run("systemctl", ["--user", "daemon-reload"]);
113
+ }
114
+ async status(profile) {
115
+ const states = serviceUnitNames(profile).map((unit) => {
116
+ const active = run("systemctl", ["--user", "is-active", unit]);
117
+ const detail = run("systemctl", ["--user", "status", unit, "--no-pager"]);
118
+ const path = systemdPathForUnit(unit);
119
+ return { unit, active, detail, path };
120
+ });
121
+ const visibleStates = states.filter((state) => existsSync(state.path) || state.active.stdout.trim() === "active");
122
+ return {
123
+ installed: states.some((state) => existsSync(state.path)),
124
+ running: states.some((state) => state.active.stdout.trim() === "active"),
125
+ detail: (visibleStates.length > 0 ? visibleStates : states).map((state) => `${state.unit}: ${state.detail.stdout || state.detail.stderr}`).join("\n"),
126
+ servicePath: systemdPath(profile)
127
+ };
128
+ }
129
+ };
130
+ var WindowsTaskServiceAdapter = class {
131
+ constructor(home) {
132
+ this.home = home;
133
+ }
134
+ home;
135
+ async install(profile, cliPath) {
136
+ const task = windowsTaskName(profile);
137
+ const logs = profilePaths(profile, this.home).logsDir;
138
+ await mkdir(logs, { recursive: true });
139
+ const command = `"${process.execPath}" "${cliPath}" run --profile "${profile}"`;
140
+ run("schtasks", ["/Create", "/TN", task, "/SC", "ONLOGON", "/TR", command, "/F"]);
141
+ }
142
+ async start(profile, cliPath) {
143
+ run("schtasks", ["/End", "/TN", legacyWindowsTaskName(profile)]);
144
+ run("schtasks", ["/Delete", "/TN", legacyWindowsTaskName(profile), "/F"]);
145
+ await this.install(profile, cliPath);
146
+ const result = run("schtasks", ["/Run", "/TN", windowsTaskName(profile)]);
147
+ if (!result.ok) throw new Error(result.stderr || "schtasks run failed");
148
+ }
149
+ async stop(profile) {
150
+ for (const task of serviceTaskNames(profile)) run("schtasks", ["/End", "/TN", task]);
151
+ }
152
+ async restart(profile, cliPath) {
153
+ await this.stop(profile);
154
+ await this.start(profile, cliPath);
155
+ }
156
+ async unregister(profile) {
157
+ for (const task of serviceTaskNames(profile)) {
158
+ run("schtasks", ["/End", "/TN", task]);
159
+ run("schtasks", ["/Delete", "/TN", task, "/F"]);
160
+ }
161
+ }
162
+ async status(profile) {
163
+ const states = serviceTaskNames(profile).map((task) => {
164
+ const result = run("schtasks", ["/Query", "/TN", task, "/V", "/FO", "LIST"]);
165
+ return { task, result };
166
+ });
167
+ const visibleStates = states.filter((state) => state.result.ok);
168
+ return {
169
+ installed: states.some((state) => state.result.ok),
170
+ running: states.some((state) => /Status:\s+Running/i.test(state.result.stdout)),
171
+ detail: (visibleStates.length > 0 ? visibleStates : states).map((state) => `${state.task}: ${state.result.stdout || state.result.stderr}`).join("\n"),
172
+ servicePath: windowsTaskName(profile)
173
+ };
174
+ }
175
+ };
176
+ var UnsupportedServiceAdapter = class {
177
+ constructor(os) {
178
+ this.os = os;
179
+ }
180
+ os;
181
+ async install() {
182
+ throw new Error(`service management is not supported on ${this.os}`);
183
+ }
184
+ async start() {
185
+ throw new Error(`service management is not supported on ${this.os}`);
186
+ }
187
+ async stop() {
188
+ throw new Error(`service management is not supported on ${this.os}`);
189
+ }
190
+ async restart() {
191
+ throw new Error(`service management is not supported on ${this.os}`);
192
+ }
193
+ async unregister() {
194
+ throw new Error(`service management is not supported on ${this.os}`);
195
+ }
196
+ async status() {
197
+ return { installed: false, running: false, detail: `unsupported platform ${this.os}` };
198
+ }
199
+ };
200
+ function run(command, args) {
201
+ const result = spawnSync(command, args, { encoding: "utf8" });
202
+ return {
203
+ ok: result.status === 0,
204
+ stdout: result.stdout ?? "",
205
+ stderr: result.stderr ?? result.error?.message ?? ""
206
+ };
207
+ }
208
+ function label(profile) {
209
+ return `com.feishu-codex-connector.${safeName(profile)}`;
210
+ }
211
+ function legacyLabel(profile) {
212
+ return `com.feishu-codex-bridge.${safeName(profile)}`;
213
+ }
214
+ function serviceLabels(profile) {
215
+ return [label(profile), legacyLabel(profile)];
216
+ }
217
+ function safeName(value) {
218
+ return value.replace(/[^a-zA-Z0-9_.-]/g, "-");
219
+ }
220
+ function launchdDomain() {
221
+ const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
222
+ return uid === void 0 ? "gui/501" : `gui/${uid}`;
223
+ }
224
+ function launchdPath(profile) {
225
+ return launchdPathForLabel(label(profile));
226
+ }
227
+ function launchdPathForLabel(serviceLabel) {
228
+ return join(homedir(), "Library", "LaunchAgents", `${serviceLabel}.plist`);
229
+ }
230
+ async function stopLaunchdLabels(labels) {
231
+ for (const serviceLabel of labels) {
232
+ const path = launchdPathForLabel(serviceLabel);
233
+ const targetResult = run("launchctl", ["bootout", `${launchdDomain()}/${serviceLabel}`]);
234
+ if (targetResult.ok || launchdStopIsMissing(targetResult)) continue;
235
+ const pathResult = existsSync(path) ? run("launchctl", ["bootout", launchdDomain(), path]) : { ok: false, stdout: "", stderr: "No such file" };
236
+ if (pathResult.ok || launchdStopIsMissing(pathResult)) continue;
237
+ throw new Error(pathResult.stderr || targetResult.stderr || "launchctl bootout failed");
238
+ }
239
+ }
240
+ function launchdStopIsMissing(result) {
241
+ return /No such process|Could not find service|No such file|does not exist|not found/i.test(result.stderr);
242
+ }
243
+ function plist(profile, cliPath, logs) {
244
+ return `<?xml version="1.0" encoding="UTF-8"?>
245
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
246
+ <plist version="1.0">
247
+ <dict>
248
+ <key>Label</key><string>${xml(label(profile))}</string>
249
+ <key>ProgramArguments</key>
250
+ <array>
251
+ <string>${xml(process.execPath)}</string>
252
+ <string>${xml(cliPath)}</string>
253
+ <string>run</string>
254
+ <string>--profile</string>
255
+ <string>${xml(profile)}</string>
256
+ </array>
257
+ <key>RunAtLoad</key><true/>
258
+ <key>KeepAlive</key><true/>
259
+ <key>StandardOutPath</key><string>${xml(join(logs, "daemon.out.log"))}</string>
260
+ <key>StandardErrorPath</key><string>${xml(join(logs, "daemon.err.log"))}</string>
261
+ </dict>
262
+ </plist>
263
+ `;
264
+ }
265
+ function systemdUnitName(profile) {
266
+ return `${label(profile)}.service`;
267
+ }
268
+ function legacySystemdUnitName(profile) {
269
+ return `${legacyLabel(profile)}.service`;
270
+ }
271
+ function serviceUnitNames(profile) {
272
+ return [systemdUnitName(profile), legacySystemdUnitName(profile)];
273
+ }
274
+ function systemdPath(profile) {
275
+ return systemdPathForUnit(systemdUnitName(profile));
276
+ }
277
+ function systemdPathForUnit(unit) {
278
+ return join(homedir(), ".config", "systemd", "user", unit);
279
+ }
280
+ function stopSystemdUnits(units) {
281
+ for (const unit of units) {
282
+ const result = run("systemctl", ["--user", "stop", unit]);
283
+ if (!result.ok && !/not loaded|not-found|could not be found/i.test(result.stderr)) throw new Error(result.stderr || "systemctl stop failed");
284
+ }
285
+ }
286
+ function systemdUnit(profile, cliPath, logs) {
287
+ return `[Unit]
288
+ Description=Feishu Codex Connector (${profile})
289
+
290
+ [Service]
291
+ ExecStart=${process.execPath} ${cliPath} run --profile ${profile}
292
+ Restart=always
293
+ RestartSec=5
294
+ StandardOutput=append:${join(logs, "daemon.out.log")}
295
+ StandardError=append:${join(logs, "daemon.err.log")}
296
+
297
+ [Install]
298
+ WantedBy=default.target
299
+ `;
300
+ }
301
+ function windowsTaskName(profile) {
302
+ return `FeishuCodexConnector-${safeName(profile)}-${basename(homedir())}`;
303
+ }
304
+ function legacyWindowsTaskName(profile) {
305
+ return `FeishuCodexBridge-${safeName(profile)}-${basename(homedir())}`;
306
+ }
307
+ function serviceTaskNames(profile) {
308
+ return [windowsTaskName(profile), legacyWindowsTaskName(profile)];
309
+ }
310
+ function xml(value) {
311
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
312
+ }
313
+
314
+ // src/cli.ts
315
+ async function main(argv = process.argv) {
316
+ const program = new Command();
317
+ program.name("feishu-codex").description("Feishu/Lark connector for Codex").version(packageVersion());
318
+ program.command("run").option("--profile <name>").option("--workspace <path>").option("--home <path>").option("--preflight", "run checks and exit").option("--bootstrap-owner", "make the first private sender an admin when no access list exists").option("--tenant <tenant>", "feishu or lark").option("--app-id <id>", "use an existing PersonalAgent app and skip QR creation").option("--app-secret <secret>", "App Secret for --app-id").option("--codex-auth <mode>", "api-key, codex-home, or inherit-user").option("--codex-api-key <key>", "OpenAI API key for api-key auth").option("--codex-path <path>", "Codex binary path override").option("--poll-chat <chatId...>", "poll specific chat IDs as a fallback when long-connection events are not configured").action(async (opts) => {
319
+ if (opts.tenant) opts.tenant = normalizeTenant(opts.tenant);
320
+ if (opts.codexAuth) opts.codexAuth = normalizeAuth(opts.codexAuth);
321
+ await runBridge(opts);
322
+ if (!opts.preflight) process.exit(0);
323
+ });
324
+ program.command("start").option("--profile <name>").option("--home <path>").action(async (opts) => {
325
+ const store = new ProfileStore(opts.home);
326
+ const profile2 = await store.read(opts.profile);
327
+ await serviceAdapter(store.paths.home).start(profile2.name, process.argv[1] ?? "feishu-codex");
328
+ console.log(`service started for ${profile2.name}`);
329
+ });
330
+ program.command("stop").option("--profile <name>").option("--home <path>").action(async (opts) => {
331
+ const store = new ProfileStore(opts.home);
332
+ const profile2 = await store.read(opts.profile);
333
+ await serviceAdapter(store.paths.home).stop(profile2.name);
334
+ console.log(`service stopped for ${profile2.name}`);
335
+ });
336
+ program.command("restart").option("--profile <name>").option("--home <path>").action(async (opts) => {
337
+ const store = new ProfileStore(opts.home);
338
+ const profile2 = await store.read(opts.profile);
339
+ await serviceAdapter(store.paths.home).restart(profile2.name, process.argv[1] ?? "feishu-codex");
340
+ console.log(`service restarted for ${profile2.name}`);
341
+ });
342
+ program.command("status").option("--profile <name>").option("--home <path>").action(async (opts) => {
343
+ const store = new ProfileStore(opts.home);
344
+ const profile2 = await store.read(opts.profile);
345
+ console.log(JSON.stringify(await serviceAdapter(store.paths.home).status(profile2.name), null, 2));
346
+ });
347
+ program.command("unregister").option("--profile <name>").option("--home <path>").action(async (opts) => {
348
+ const store = new ProfileStore(opts.home);
349
+ const profile2 = await store.read(opts.profile);
350
+ await serviceAdapter(store.paths.home).unregister(profile2.name);
351
+ console.log(`service unregistered for ${profile2.name}`);
352
+ });
353
+ program.command("ps").option("--home <path>").action(async (opts) => {
354
+ const rows = await new RuntimeRegistry(new ProfileStore(opts.home).paths.home).list();
355
+ if (rows.length === 0) {
356
+ console.log("no running connector processes");
357
+ return;
358
+ }
359
+ for (const row of rows) {
360
+ console.log(`${row.id} pid:${row.pid} profile:${row.profile} app:*${row.appId.slice(-6)} log:${row.logPath ?? "-"}`);
361
+ }
362
+ });
363
+ program.command("kill <target>").option("--home <path>").action(async (target, opts) => {
364
+ const registry = new RuntimeRegistry(new ProfileStore(opts.home).paths.home);
365
+ const killed = await registry.kill(target);
366
+ if (!killed) throw new Error(`process not found: ${target}`);
367
+ console.log(`sent SIGTERM to ${killed.id} pid:${killed.pid}`);
368
+ });
369
+ program.command("doctor").option("--profile <name>").option("--home <path>").option("--json").action(async (opts) => {
370
+ const store = new ProfileStore(opts.home);
371
+ const profile2 = await store.read(opts.profile);
372
+ const runner = runnerForProfile(profile2);
373
+ const availability = await runner.checkAvailability();
374
+ const workspace = await resolveWorkspace(profile2.workspaces.default);
375
+ const processes = await new RuntimeRegistry(store.paths.home).list();
376
+ const report = {
377
+ profile: profile2.name,
378
+ tenant: profile2.tenant,
379
+ appIdSuffix: profile2.app.id.slice(-6),
380
+ backend: availability,
381
+ workspace,
382
+ service: await serviceAdapter(store.paths.home).status(profile2.name),
383
+ processes,
384
+ codexAuth: profile2.codex.auth.mode,
385
+ larkCli: profile2.feishuCli.identityPreset
386
+ };
387
+ if (opts.json) console.log(JSON.stringify(report, null, 2));
388
+ else {
389
+ console.log(`Profile: ${report.profile}`);
390
+ console.log(`Tenant: ${report.tenant}`);
391
+ console.log(`Backend: ${availability.label} (${availability.ok ? "ok" : "failed"})`);
392
+ console.log(`Workspace: ${workspace.ok ? workspace.cwdRealpath : workspace.userVisible}`);
393
+ console.log(`Service: ${report.service.installed ? "installed" : "not installed"} / ${report.service.running ? "running" : "stopped"}`);
394
+ console.log(`Processes: ${processes.length}`);
395
+ }
396
+ });
397
+ const profile = program.command("profile").description("Manage local profiles");
398
+ profile.command("create <name>").option("--tenant <tenant>", "feishu or lark", "feishu").requiredOption("--app-id <id>").requiredOption("--app-secret <secret>").option("--codex-auth <mode>", "api-key, codex-home, or inherit-user", "api-key").option("--codex-api-key <key>").option("--codex-backend <backend>", "sdk or exec-json", "sdk").option("--codex-path <path>").option("--workspace <path>").option("--home <path>").action(async (name, opts) => {
399
+ const tenant = normalizeTenant(opts.tenant);
400
+ const auth = normalizeAuth(opts.codexAuth);
401
+ const store = new ProfileStore(opts.home);
402
+ const created = await store.create({
403
+ name,
404
+ tenant,
405
+ appId: opts.appId,
406
+ appSecret: opts.appSecret,
407
+ codexAuthMode: auth,
408
+ codexApiKey: opts.codexApiKey,
409
+ workspace: opts.workspace,
410
+ codexBackend: opts.codexBackend,
411
+ codexPath: opts.codexPath
412
+ });
413
+ console.log(`created profile ${created.name}`);
414
+ });
415
+ profile.command("list").option("--home <path>").action(async (opts) => {
416
+ const store = new ProfileStore(opts.home);
417
+ const rows = await store.list();
418
+ for (const row of rows) {
419
+ console.log(`${row.active ? "*" : " "} ${row.name} ${row.tenant} app:*${row.appIdSuffix} workspace:${row.workspaceStatus} ${row.backend} ${row.codexAuthMode} lark-cli:${row.feishuCliIdentity}`);
420
+ }
421
+ });
422
+ profile.command("use <name>").option("--home <path>").action(async (name, opts) => {
423
+ await new ProfileStore(opts.home).use(name);
424
+ console.log(`active profile: ${name}`);
425
+ });
426
+ profile.command("remove <name>").option("--purge").option("--yes").option("--home <path>").action(async (name, opts) => {
427
+ const result = await new ProfileStore(opts.home).remove(name, { purge: opts.purge, yes: opts.yes });
428
+ console.log(result);
429
+ });
430
+ profile.command("export <name>").option("--include-secrets").option("--yes").option("--home <path>").action(async (name, opts) => {
431
+ if (opts.includeSecrets && !opts.yes) throw new Error("--include-secrets requires --yes");
432
+ console.log(JSON.stringify(await new ProfileStore(opts.home).export(name, { includeSecrets: opts.includeSecrets }), null, 2));
433
+ });
434
+ await program.parseAsync(argv);
435
+ }
436
+ function packageVersion() {
437
+ try {
438
+ const require2 = createRequire(import.meta.url);
439
+ return require2("../package.json").version ?? "0.0.0";
440
+ } catch {
441
+ return "0.0.0";
442
+ }
443
+ }
444
+ function normalizeTenant(value) {
445
+ if (value === "feishu" || value === "lark") return value;
446
+ throw new Error("tenant must be feishu or lark");
447
+ }
448
+ function normalizeAuth(value) {
449
+ if (value === "api-key" || value === "codex-home" || value === "inherit-user") return value;
450
+ throw new Error("codex auth must be api-key, codex-home, or inherit-user");
451
+ }
452
+ export {
453
+ main
454
+ };