@keyou007/agent-bot 0.1.14-alpha.1 → 0.1.14-alpha.2

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 (67) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +42 -12
  3. package/README.zh.md +42 -12
  4. package/config.example.yaml +1 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +17 -13
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/Initializer.js +35 -0
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/SelfUpdateRunner.js +552 -0
  12. package/dist/cli/SelfUpdateRunner.js.map +1 -0
  13. package/dist/cli/SelfUpdater.js +431 -0
  14. package/dist/cli/SelfUpdater.js.map +1 -0
  15. package/dist/cli/autostartOutput.js +63 -0
  16. package/dist/cli/autostartOutput.js.map +1 -0
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +92 -18
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +14 -11
  21. package/dist/cli/taskGroupOptions.js.map +1 -1
  22. package/dist/cli/taskListOutput.js +3 -0
  23. package/dist/cli/taskListOutput.js.map +1 -1
  24. package/dist/cli/taskTarget.js +99 -0
  25. package/dist/cli/taskTarget.js.map +1 -0
  26. package/dist/cli.js +643 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +8 -2
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +7 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/feishu/CardRenderer.js +172 -23
  33. package/dist/feishu/CardRenderer.js.map +1 -1
  34. package/dist/feishu/FeishuMarkdown.js +25 -1
  35. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  36. package/dist/index.js +134 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/proxy/ProxySessionController.js +436 -12
  39. package/dist/proxy/ProxySessionController.js.map +1 -1
  40. package/dist/state/StateStore.js +18 -0
  41. package/dist/state/StateStore.js.map +1 -1
  42. package/docs/technical-reference.md +17 -7
  43. package/docs/technical-reference.zh.md +17 -7
  44. package/npm-shrinkwrap.json +2 -2
  45. package/package.json +2 -2
  46. package/skills/agent-bot/SKILL.md +107 -188
  47. package/src/cli/AutostartManager.ts +579 -0
  48. package/src/cli/FeishuAppConfiguration.ts +22 -7
  49. package/src/cli/Initializer.ts +57 -0
  50. package/src/cli/SelfUpdateRunner.ts +817 -0
  51. package/src/cli/SelfUpdater.ts +688 -0
  52. package/src/cli/autostartOutput.ts +75 -0
  53. package/src/cli/controlProtocol.ts +111 -1
  54. package/src/cli/help.ts +92 -18
  55. package/src/cli/taskGroupOptions.ts +27 -17
  56. package/src/cli/taskListOutput.ts +10 -0
  57. package/src/cli/taskTarget.ts +168 -0
  58. package/src/cli.ts +781 -65
  59. package/src/codex/CodexRuntime.ts +8 -2
  60. package/src/commands/CommandRouter.ts +6 -1
  61. package/src/commands/commandTypes.ts +1 -0
  62. package/src/feishu/CardRenderer.ts +199 -23
  63. package/src/feishu/FeishuMarkdown.ts +27 -1
  64. package/src/index.ts +172 -2
  65. package/src/proxy/ProxySessionController.ts +532 -15
  66. package/src/runtime/types.ts +1 -0
  67. package/src/state/StateStore.ts +19 -0
@@ -0,0 +1,579 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import type { AppConfig } from "../config/schema.js";
8
+ import {
9
+ AGENT_BOT_EXPLICIT_PROFILE_ENV,
10
+ AGENT_BOT_HOME_ENV,
11
+ agentBotHome,
12
+ defaultConfigPath,
13
+ resolveUserPath,
14
+ } from "../config/paths.js";
15
+ import {
16
+ nodeDiagnosticReportArguments,
17
+ resolveSupervisorDiagnosticsPaths,
18
+ } from "../supervision/SupervisorDiagnostics.js";
19
+
20
+ export type SupportedAutostartPlatform = "win32" | "darwin" | "linux";
21
+
22
+ export interface AutostartContext {
23
+ profilePath: string;
24
+ configPath: string;
25
+ nodePath: string;
26
+ supervisorPath: string;
27
+ crashReportDirectory: string;
28
+ pathValue?: string;
29
+ }
30
+
31
+ export interface AutostartPlan {
32
+ platform: SupportedAutostartPlatform;
33
+ name: string;
34
+ profilePath: string;
35
+ configPath: string;
36
+ bootstrapPath: string;
37
+ bootstrapContent: string;
38
+ definitionPath: string;
39
+ definitionContent?: string;
40
+ command: string;
41
+ commandArguments: string[];
42
+ windowsFallbackPath?: string;
43
+ windowsFallbackContent?: string;
44
+ }
45
+
46
+ export interface AutostartRegistrationStatus {
47
+ supported: boolean;
48
+ platform: NodeJS.Platform;
49
+ profilePath: string;
50
+ configPath: string;
51
+ enabled: boolean;
52
+ loaded?: boolean;
53
+ linger?: boolean;
54
+ name?: string;
55
+ definitionPath?: string;
56
+ trigger?: "login" | "boot";
57
+ mechanism?: "task-scheduler" | "startup-folder" | "launch-agent" | "systemd";
58
+ }
59
+
60
+ export interface EnableAutostartOptions {
61
+ linger?: boolean;
62
+ }
63
+
64
+ interface CommandResult {
65
+ status: number | null;
66
+ stdout: string;
67
+ stderr: string;
68
+ error?: Error;
69
+ }
70
+
71
+ interface AutostartDependencies {
72
+ platform: NodeJS.Platform;
73
+ homeDirectory: string;
74
+ username: string;
75
+ uid?: number;
76
+ appDataDirectory?: string;
77
+ run(command: string, args: string[]): CommandResult;
78
+ exists(filePath: string): boolean;
79
+ write(filePath: string, content: string): void;
80
+ remove(filePath: string): void;
81
+ removeDirectoryIfEmpty(directoryPath: string): void;
82
+ }
83
+
84
+ export type AutostartDependencyOverrides = Partial<AutostartDependencies>;
85
+
86
+ export class AutostartManager {
87
+ readonly plan?: AutostartPlan;
88
+ private readonly dependencies: AutostartDependencies;
89
+
90
+ constructor(
91
+ private readonly context: AutostartContext,
92
+ overrides: AutostartDependencyOverrides = {},
93
+ ) {
94
+ this.dependencies = { ...defaultDependencies(), ...overrides };
95
+ if (isSupportedPlatform(this.dependencies.platform)) {
96
+ this.plan = createAutostartPlan(
97
+ context,
98
+ this.dependencies.platform,
99
+ this.dependencies.homeDirectory,
100
+ this.dependencies.appDataDirectory,
101
+ );
102
+ }
103
+ }
104
+
105
+ enable(options: EnableAutostartOptions = {}): AutostartRegistrationStatus {
106
+ const plan = this.requirePlan();
107
+ if (options.linger && plan.platform !== "linux") {
108
+ throw new Error("--linger is supported only on Linux.");
109
+ }
110
+
111
+ this.dependencies.write(plan.bootstrapPath, plan.bootstrapContent);
112
+ if (plan.definitionContent !== undefined) {
113
+ this.dependencies.write(plan.definitionPath, plan.definitionContent);
114
+ }
115
+
116
+ if (plan.platform === "win32") {
117
+ const task = this.dependencies.run("schtasks.exe", [
118
+ "/Create",
119
+ "/TN",
120
+ plan.name,
121
+ "/SC",
122
+ "ONLOGON",
123
+ "/TR",
124
+ windowsCommandLine(plan.command, plan.commandArguments),
125
+ "/RL",
126
+ "LIMITED",
127
+ "/F",
128
+ ]);
129
+ if (!task.error && task.status === 0) {
130
+ if (plan.windowsFallbackPath) this.dependencies.remove(plan.windowsFallbackPath);
131
+ } else if (plan.windowsFallbackPath && plan.windowsFallbackContent) {
132
+ this.dependencies.write(plan.windowsFallbackPath, plan.windowsFallbackContent);
133
+ } else {
134
+ this.throwCommandError(task, "Could not register the Windows startup task");
135
+ }
136
+ } else if (plan.platform === "linux") {
137
+ this.runChecked("systemctl", ["--user", "daemon-reload"], "Could not reload systemd user units");
138
+ this.runChecked("systemctl", ["--user", "enable", plan.name], "Could not enable the systemd user unit");
139
+ if (options.linger) {
140
+ this.runChecked(
141
+ "loginctl",
142
+ ["enable-linger", this.dependencies.username],
143
+ "Could not enable systemd user lingering",
144
+ );
145
+ }
146
+ }
147
+
148
+ return this.status();
149
+ }
150
+
151
+ disable(): AutostartRegistrationStatus {
152
+ const plan = this.requirePlan();
153
+ const current = this.status();
154
+
155
+ if (plan.platform === "win32") {
156
+ const taskRegistered = this.dependencies.run("schtasks.exe", ["/Query", "/TN", plan.name]).status === 0;
157
+ if (taskRegistered) {
158
+ this.runChecked(
159
+ "schtasks.exe",
160
+ ["/Delete", "/TN", plan.name, "/F"],
161
+ "Could not remove the Windows startup task",
162
+ );
163
+ }
164
+ if (plan.windowsFallbackPath) this.dependencies.remove(plan.windowsFallbackPath);
165
+ } else if (plan.platform === "linux") {
166
+ if (current.enabled) {
167
+ this.runChecked(
168
+ "systemctl",
169
+ ["--user", "disable", plan.name],
170
+ "Could not disable the systemd user unit",
171
+ );
172
+ }
173
+ this.dependencies.remove(plan.definitionPath);
174
+ this.runChecked("systemctl", ["--user", "daemon-reload"], "Could not reload systemd user units");
175
+ } else if (plan.platform === "darwin") {
176
+ this.dependencies.remove(plan.definitionPath);
177
+ }
178
+
179
+ this.dependencies.remove(plan.bootstrapPath);
180
+ this.dependencies.removeDirectoryIfEmpty(path.dirname(plan.bootstrapPath));
181
+ return this.status();
182
+ }
183
+
184
+ status(): AutostartRegistrationStatus {
185
+ const platform = this.dependencies.platform;
186
+ if (!this.plan) {
187
+ return {
188
+ supported: false,
189
+ platform,
190
+ profilePath: this.context.profilePath,
191
+ configPath: this.context.configPath,
192
+ enabled: false,
193
+ };
194
+ }
195
+
196
+ const plan = this.plan;
197
+ if (plan.platform === "win32") {
198
+ const task = this.dependencies.run("schtasks.exe", ["/Query", "/TN", plan.name]);
199
+ const fallback = plan.windowsFallbackPath
200
+ ? this.dependencies.exists(plan.windowsFallbackPath)
201
+ : false;
202
+ return this.registrationStatus(task.status === 0 || fallback, {
203
+ mechanism: task.status === 0 ? "task-scheduler" : fallback ? "startup-folder" : undefined,
204
+ definitionPath: fallback ? plan.windowsFallbackPath : plan.definitionPath,
205
+ });
206
+ }
207
+
208
+ if (plan.platform === "darwin") {
209
+ const domain = `gui/${this.dependencies.uid ?? 0}/${plan.name}`;
210
+ const loaded = this.dependencies.run("launchctl", ["print", domain]).status === 0;
211
+ return this.registrationStatus(this.dependencies.exists(plan.definitionPath), {
212
+ loaded,
213
+ mechanism: "launch-agent",
214
+ });
215
+ }
216
+
217
+ const enabled = this.dependencies.run("systemctl", ["--user", "is-enabled", plan.name]).status === 0;
218
+ const loaded = this.dependencies.run("systemctl", ["--user", "is-active", plan.name]).status === 0;
219
+ const lingerResult = this.dependencies.run("loginctl", [
220
+ "show-user",
221
+ this.dependencies.username,
222
+ "--property=Linger",
223
+ "--value",
224
+ ]);
225
+ const linger = lingerResult.status === 0 && lingerResult.stdout.trim().toLowerCase() === "yes";
226
+ return this.registrationStatus(enabled, {
227
+ loaded,
228
+ linger,
229
+ trigger: linger ? "boot" : "login",
230
+ mechanism: "systemd",
231
+ });
232
+ }
233
+
234
+ private requirePlan(): AutostartPlan {
235
+ if (!this.plan) {
236
+ throw new Error(`Autostart is not supported on ${this.dependencies.platform}.`);
237
+ }
238
+ return this.plan;
239
+ }
240
+
241
+ private registrationStatus(
242
+ enabled: boolean,
243
+ details: Pick<
244
+ AutostartRegistrationStatus,
245
+ "loaded" | "linger" | "trigger" | "mechanism" | "definitionPath"
246
+ > = {},
247
+ ): AutostartRegistrationStatus {
248
+ const plan = this.requirePlan();
249
+ return {
250
+ supported: true,
251
+ platform: plan.platform,
252
+ profilePath: plan.profilePath,
253
+ configPath: plan.configPath,
254
+ enabled,
255
+ name: plan.name,
256
+ definitionPath: details.definitionPath ?? plan.definitionPath,
257
+ trigger: details.trigger ?? "login",
258
+ ...(details.mechanism === undefined ? {} : { mechanism: details.mechanism }),
259
+ ...(details.loaded === undefined ? {} : { loaded: details.loaded }),
260
+ ...(details.linger === undefined ? {} : { linger: details.linger }),
261
+ };
262
+ }
263
+
264
+ private runChecked(command: string, args: string[], description: string): void {
265
+ const result = this.dependencies.run(command, args);
266
+ if (!result.error && result.status === 0) return;
267
+ this.throwCommandError(result, description);
268
+ }
269
+
270
+ private throwCommandError(result: CommandResult, description: string): never {
271
+ const detail = result.error?.message || result.stderr.trim() || result.stdout.trim()
272
+ || `exit code ${result.status ?? "unknown"}`;
273
+ throw new Error(`${description}: ${detail}`);
274
+ }
275
+ }
276
+
277
+ export function createCurrentAutostartManager(
278
+ config: AppConfig,
279
+ overrides: AutostartDependencyOverrides = {},
280
+ ): AutostartManager {
281
+ const configuredPath = process.env.AGENT_BOT_CONFIG?.trim();
282
+ const profilePath = agentBotHome();
283
+ const configPath = configuredPath ? resolveUserPath(configuredPath) : defaultConfigPath();
284
+ return new AutostartManager({
285
+ profilePath,
286
+ configPath,
287
+ nodePath: process.execPath,
288
+ supervisorPath: fileURLToPath(new URL("../supervisor.js", import.meta.url)),
289
+ crashReportDirectory: resolveSupervisorDiagnosticsPaths(config).crashReportDirectory,
290
+ pathValue: process.env.PATH,
291
+ }, overrides);
292
+ }
293
+
294
+ export function createAutostartPlan(
295
+ context: AutostartContext,
296
+ platform: SupportedAutostartPlatform,
297
+ homeDirectory: string,
298
+ appDataDirectory?: string,
299
+ ): AutostartPlan {
300
+ const pathApi = platform === "win32" ? path.win32 : path.posix;
301
+ const profilePath = pathApi.resolve(context.profilePath);
302
+ const configPath = pathApi.resolve(context.configPath);
303
+ const bootstrapPath = pathApi.join(profilePath, "autostart", "supervisor-bootstrap.mjs");
304
+ const identity = createHash("sha256")
305
+ .update(`${platform}\0${profilePath}\0${configPath}`)
306
+ .digest("hex")
307
+ .slice(0, 10);
308
+ const bootstrapContent = renderBootstrap({ ...context, profilePath, configPath }, platform);
309
+ const commandArguments = [
310
+ ...nodeDiagnosticReportArguments(context.crashReportDirectory),
311
+ bootstrapPath,
312
+ ];
313
+
314
+ if (platform === "win32") {
315
+ const profileName = sanitizeName(pathApi.basename(profilePath).replace(/^\.+/, "") || "main");
316
+ const name = `AgentBot-${profileName}-${identity}`;
317
+ const fallbackPath = pathApi.join(
318
+ appDataDirectory ?? pathApi.join(homeDirectory, "AppData", "Roaming"),
319
+ "Microsoft",
320
+ "Windows",
321
+ "Start Menu",
322
+ "Programs",
323
+ "Startup",
324
+ `${name}.vbs`,
325
+ );
326
+ return {
327
+ platform,
328
+ name,
329
+ profilePath,
330
+ configPath,
331
+ bootstrapPath,
332
+ bootstrapContent,
333
+ definitionPath: bootstrapPath,
334
+ command: context.nodePath,
335
+ commandArguments,
336
+ windowsFallbackPath: fallbackPath,
337
+ windowsFallbackContent: renderWindowsStartupScript(context.nodePath, commandArguments),
338
+ };
339
+ }
340
+
341
+ if (platform === "darwin") {
342
+ const name = `com.keyou.agent-bot.${identity}`;
343
+ const definitionPath = pathApi.join(homeDirectory, "Library", "LaunchAgents", `${name}.plist`);
344
+ const stdoutPath = pathApi.join(profilePath, "logs", "autostart.stdout.log");
345
+ const stderrPath = pathApi.join(profilePath, "logs", "autostart.stderr.log");
346
+ return {
347
+ platform,
348
+ name,
349
+ profilePath,
350
+ configPath,
351
+ bootstrapPath,
352
+ bootstrapContent,
353
+ definitionPath,
354
+ definitionContent: renderLaunchAgent({
355
+ name,
356
+ command: context.nodePath,
357
+ commandArguments,
358
+ workingDirectory: profilePath,
359
+ stdoutPath,
360
+ stderrPath,
361
+ }),
362
+ command: context.nodePath,
363
+ commandArguments,
364
+ };
365
+ }
366
+
367
+ const name = `agent-bot-${identity}.service`;
368
+ const definitionPath = pathApi.join(homeDirectory, ".config", "systemd", "user", name);
369
+ return {
370
+ platform,
371
+ name,
372
+ profilePath,
373
+ configPath,
374
+ bootstrapPath,
375
+ bootstrapContent,
376
+ definitionPath,
377
+ definitionContent: renderSystemdUnit({
378
+ profilePath,
379
+ command: context.nodePath,
380
+ commandArguments,
381
+ }),
382
+ command: context.nodePath,
383
+ commandArguments,
384
+ };
385
+ }
386
+
387
+ function renderBootstrap(context: AutostartContext, platform: SupportedAutostartPlatform): string {
388
+ return [
389
+ `process.env.${AGENT_BOT_HOME_ENV} = ${JSON.stringify(context.profilePath)};`,
390
+ `process.env.AGENT_BOT_CONFIG = ${JSON.stringify(context.configPath)};`,
391
+ `process.env.${AGENT_BOT_EXPLICIT_PROFILE_ENV} = "1";`,
392
+ ...(context.pathValue?.trim()
393
+ ? [`process.env.PATH = ${JSON.stringify(context.pathValue)};`]
394
+ : []),
395
+ "delete process.env.FEISHU_APP_ID;",
396
+ "delete process.env.FEISHU_APP_SECRET;",
397
+ "delete process.env.FEISHU_USER_OPEN_ID;",
398
+ `process.chdir(${JSON.stringify(context.profilePath)});`,
399
+ `await import(${JSON.stringify(fileUrlForPlatform(context.supervisorPath, platform))});`,
400
+ "",
401
+ ].join("\n");
402
+ }
403
+
404
+ function fileUrlForPlatform(filePath: string, platform: SupportedAutostartPlatform): string {
405
+ if (platform !== "win32") return pathToFileURL(filePath).href;
406
+ const normalized = filePath.replaceAll("\\", "/");
407
+ if (normalized.startsWith("//")) {
408
+ const [host = "", ...segments] = normalized.slice(2).split("/");
409
+ return `file://${host}/${segments.map(encodeURIComponent).join("/")}`;
410
+ }
411
+ const drivePath = /^([A-Za-z]):\/(.*)$/.exec(normalized);
412
+ if (!drivePath) return pathToFileURL(filePath).href;
413
+ return `file:///${drivePath[1]}:/${(drivePath[2] ?? "").split("/").map(encodeURIComponent).join("/")}`;
414
+ }
415
+
416
+ function renderLaunchAgent(input: {
417
+ name: string;
418
+ command: string;
419
+ commandArguments: string[];
420
+ workingDirectory: string;
421
+ stdoutPath: string;
422
+ stderrPath: string;
423
+ }): string {
424
+ const argumentsXml = [input.command, ...input.commandArguments]
425
+ .map((value) => ` <string>${escapeXml(value)}</string>`)
426
+ .join("\n");
427
+ return `<?xml version="1.0" encoding="UTF-8"?>
428
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
429
+ <plist version="1.0">
430
+ <dict>
431
+ <key>Label</key>
432
+ <string>${escapeXml(input.name)}</string>
433
+ <key>ProgramArguments</key>
434
+ <array>
435
+ ${argumentsXml}
436
+ </array>
437
+ <key>WorkingDirectory</key>
438
+ <string>${escapeXml(input.workingDirectory)}</string>
439
+ <key>RunAtLoad</key>
440
+ <true/>
441
+ <key>KeepAlive</key>
442
+ <false/>
443
+ <key>ProcessType</key>
444
+ <string>Background</string>
445
+ <key>StandardOutPath</key>
446
+ <string>${escapeXml(input.stdoutPath)}</string>
447
+ <key>StandardErrorPath</key>
448
+ <string>${escapeXml(input.stderrPath)}</string>
449
+ </dict>
450
+ </plist>
451
+ `;
452
+ }
453
+
454
+ function renderSystemdUnit(input: {
455
+ profilePath: string;
456
+ command: string;
457
+ commandArguments: string[];
458
+ }): string {
459
+ const commandLine = [input.command, ...input.commandArguments].map(systemdQuote).join(" ");
460
+ return `[Unit]
461
+ Description=Agent Bot Supervisor
462
+ Wants=network-online.target
463
+ After=network-online.target
464
+
465
+ [Service]
466
+ Type=simple
467
+ WorkingDirectory=${systemdQuote(input.profilePath)}
468
+ ExecStart=${commandLine}
469
+ Restart=no
470
+
471
+ [Install]
472
+ WantedBy=default.target
473
+ `;
474
+ }
475
+
476
+ function defaultDependencies(): AutostartDependencies {
477
+ return {
478
+ platform: process.platform,
479
+ homeDirectory: os.homedir(),
480
+ username: currentUsername(),
481
+ uid: process.getuid?.(),
482
+ appDataDirectory: process.env.APPDATA?.trim(),
483
+ run: (command, args) => {
484
+ const result = spawnSync(command, args, {
485
+ encoding: "utf8",
486
+ windowsHide: true,
487
+ timeout: 15_000,
488
+ });
489
+ return {
490
+ status: result.status,
491
+ stdout: result.stdout ?? "",
492
+ stderr: result.stderr ?? "",
493
+ ...(result.error ? { error: result.error } : {}),
494
+ };
495
+ },
496
+ exists: (filePath) => fs.existsSync(filePath),
497
+ write: (filePath, content) => writePrivateFile(filePath, content),
498
+ remove: (filePath) => fs.rmSync(filePath, { force: true }),
499
+ removeDirectoryIfEmpty: (directoryPath) => {
500
+ try {
501
+ fs.rmdirSync(directoryPath);
502
+ } catch {
503
+ // The directory is shared with a newer registration or contains user files.
504
+ }
505
+ },
506
+ };
507
+ }
508
+
509
+ function writePrivateFile(filePath: string, content: string): void {
510
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
511
+ fs.writeFileSync(filePath, content, { encoding: "utf8", mode: 0o600 });
512
+ try {
513
+ fs.chmodSync(filePath, 0o600);
514
+ } catch {
515
+ // Windows and some mounted filesystems do not implement POSIX modes.
516
+ }
517
+ }
518
+
519
+ function currentUsername(): string {
520
+ try {
521
+ return os.userInfo().username;
522
+ } catch {
523
+ return process.env.USER?.trim() || process.env.USERNAME?.trim() || "";
524
+ }
525
+ }
526
+
527
+ function isSupportedPlatform(platform: NodeJS.Platform): platform is SupportedAutostartPlatform {
528
+ return platform === "win32" || platform === "darwin" || platform === "linux";
529
+ }
530
+
531
+ function sanitizeName(value: string): string {
532
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "main";
533
+ }
534
+
535
+ function escapeXml(value: string): string {
536
+ return value
537
+ .replaceAll("&", "&amp;")
538
+ .replaceAll("<", "&lt;")
539
+ .replaceAll(">", "&gt;")
540
+ .replaceAll("\"", "&quot;")
541
+ .replaceAll("'", "&apos;");
542
+ }
543
+
544
+ function systemdQuote(value: string): string {
545
+ return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("%", "%%")}"`;
546
+ }
547
+
548
+ function windowsCommandLine(command: string, args: string[]): string {
549
+ return [command, ...args].map(quoteWindowsArgument).join(" ");
550
+ }
551
+
552
+ function renderWindowsStartupScript(command: string, args: string[]): string {
553
+ const commandLine = windowsCommandLine(command, args).replaceAll("\"", "\"\"");
554
+ return [
555
+ "Set shell = CreateObject(\"WScript.Shell\")",
556
+ `shell.Run "${commandLine}", 0, False`,
557
+ "",
558
+ ].join("\r\n");
559
+ }
560
+
561
+ function quoteWindowsArgument(value: string): string {
562
+ if (value && !/[\s"]/u.test(value)) return value;
563
+ let result = "\"";
564
+ let backslashes = 0;
565
+ for (const character of value) {
566
+ if (character === "\\") {
567
+ backslashes += 1;
568
+ continue;
569
+ }
570
+ if (character === "\"") {
571
+ result += "\\".repeat(backslashes * 2 + 1) + "\"";
572
+ backslashes = 0;
573
+ continue;
574
+ }
575
+ result += "\\".repeat(backslashes) + character;
576
+ backslashes = 0;
577
+ }
578
+ return `${result}${"\\".repeat(backslashes * 2)}\"`;
579
+ }
@@ -72,7 +72,8 @@ const CORE_FEISHU_SCOPES = new Set([
72
72
  "im:message:send_as_bot",
73
73
  ]);
74
74
  const CORE_FEISHU_EVENTS = new Set<string>(["im.message.receive_v1"]);
75
- const MANUAL_FEISHU_SCOPES = new Set(["im:message.group_msg"]);
75
+ const ALL_GROUP_MESSAGE_SCOPES = new Set(["im:message.group_msg"]);
76
+ const MANUAL_FEISHU_SCOPES = ALL_GROUP_MESSAGE_SCOPES;
76
77
 
77
78
  export interface FeishuAppConfiguration {
78
79
  scopes: string[];
@@ -96,6 +97,7 @@ export interface FeishuConfigurationChallenge {
96
97
  export interface EnsureFeishuAppConfigurationOptions {
97
98
  fetch?: typeof globalThis.fetch;
98
99
  sleep?: (milliseconds: number) => Promise<void>;
100
+ respondToAllGroupMessages?: boolean;
99
101
  signal?: AbortSignal;
100
102
  manualPermissionSkipSignal?: AbortSignal;
101
103
  optionalSkipSignal?: AbortSignal;
@@ -124,6 +126,9 @@ export async function ensureFeishuAppConfiguration(
124
126
  const timeoutMs = positiveInteger(options.timeoutMs) ?? DEFAULT_COMPLETION_TIMEOUT_MS;
125
127
  const optionalTimeoutMs = positiveInteger(options.optionalTimeoutMs)
126
128
  ?? DEFAULT_OPTIONAL_COMPLETION_TIMEOUT_MS;
129
+ const excludedScopes = options.respondToAllGroupMessages === false
130
+ ? ALL_GROUP_MESSAGE_SCOPES
131
+ : new Set<string>();
127
132
 
128
133
  let configuration: FeishuAppConfiguration;
129
134
  try {
@@ -133,7 +138,7 @@ export async function ensureFeishuAppConfiguration(
133
138
  configuration = emptyConfiguration();
134
139
  }
135
140
 
136
- const missing = missingFeishuAppConfiguration(configuration);
141
+ const missing = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
137
142
  if (!hasMissingConfiguration(missing)) {
138
143
  return {
139
144
  status: "ready",
@@ -154,6 +159,8 @@ export async function ensureFeishuAppConfiguration(
154
159
  pollIntervalMs,
155
160
  optionalTimeoutMs,
156
161
  options,
162
+ new Set(),
163
+ excludedScopes,
157
164
  );
158
165
  }
159
166
 
@@ -179,7 +186,7 @@ export async function ensureFeishuAppConfiguration(
179
186
 
180
187
  try {
181
188
  configuration = await readFeishuAppConfiguration(credentials, fetchImpl, options.signal);
182
- const remaining = missingFeishuAppConfiguration(configuration);
189
+ const remaining = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
183
190
  const unresolvedCore = coreMissingFeishuAppConfiguration(remaining);
184
191
  if (!hasBlockingCoreConfiguration(unresolvedCore, options.manualPermissionSkipSignal)) {
185
192
  return completeOptionalFeishuConfiguration(
@@ -192,12 +199,16 @@ export async function ensureFeishuAppConfiguration(
192
199
  optionalTimeoutMs,
193
200
  options,
194
201
  skippedManualScopes(coreMissing, options.manualPermissionSkipSignal),
202
+ excludedScopes,
195
203
  );
196
204
  }
197
205
  lastError = undefined;
198
206
  } catch (error) {
199
207
  if (options.signal?.aborted) throw abortError(options.signal);
200
- const knownRemaining = missingFeishuAppConfiguration(configuration);
208
+ const knownRemaining = withoutIgnoredScopes(
209
+ missingFeishuAppConfiguration(configuration),
210
+ excludedScopes,
211
+ );
201
212
  if (!hasBlockingCoreConfiguration(
202
213
  coreMissingFeishuAppConfiguration(knownRemaining),
203
214
  options.manualPermissionSkipSignal,
@@ -212,6 +223,7 @@ export async function ensureFeishuAppConfiguration(
212
223
  optionalTimeoutMs,
213
224
  options,
214
225
  skippedManualScopes(coreMissing, options.manualPermissionSkipSignal),
226
+ excludedScopes,
215
227
  );
216
228
  }
217
229
  lastError = error;
@@ -267,9 +279,10 @@ async function completeOptionalFeishuConfiguration(
267
279
  timeoutMs: number,
268
280
  options: EnsureFeishuAppConfigurationOptions,
269
281
  ignoredScopes: ReadonlySet<string> = new Set(),
282
+ excludedScopes: ReadonlySet<string> = new Set(),
270
283
  ): Promise<EnsureFeishuAppConfigurationResult> {
271
284
  let configuration = initialConfiguration;
272
- let remaining = missingFeishuAppConfiguration(configuration);
285
+ let remaining = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
273
286
  const requested = withoutIgnoredScopes(remaining, ignoredScopes);
274
287
  if (hasMissingConfiguration(requested)) {
275
288
  await requestMissingFeishuConfiguration(
@@ -289,6 +302,7 @@ async function completeOptionalFeishuConfiguration(
289
302
  options.signal,
290
303
  options.optionalSkipSignal,
291
304
  ignoredScopes,
305
+ excludedScopes,
292
306
  );
293
307
  configuration = completed.configuration;
294
308
  remaining = completed.remaining;
@@ -312,12 +326,13 @@ async function waitForOptionalFeishuConfiguration(
312
326
  signal?: AbortSignal,
313
327
  optionalSkipSignal?: AbortSignal,
314
328
  ignoredScopes: ReadonlySet<string> = new Set(),
329
+ excludedScopes: ReadonlySet<string> = new Set(),
315
330
  ): Promise<{
316
331
  configuration: FeishuAppConfiguration;
317
332
  remaining: MissingFeishuAppConfiguration;
318
333
  }> {
319
334
  let configuration = initialConfiguration;
320
- let remaining = missingFeishuAppConfiguration(configuration);
335
+ let remaining = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
321
336
  let remainingMs = timeoutMs;
322
337
  while (remainingMs > 0 && hasMissingConfiguration(withoutIgnoredScopes(remaining, ignoredScopes))) {
323
338
  if (optionalSkipSignal?.aborted) break;
@@ -332,7 +347,7 @@ async function waitForOptionalFeishuConfiguration(
332
347
  remainingMs -= waitMs;
333
348
  try {
334
349
  configuration = await readFeishuAppConfiguration(credentials, fetchImpl, signal);
335
- remaining = missingFeishuAppConfiguration(configuration);
350
+ remaining = withoutIgnoredScopes(missingFeishuAppConfiguration(configuration), excludedScopes);
336
351
  } catch (error) {
337
352
  if (signal?.aborted) throw abortError(signal);
338
353
  }