feishu-codex-console 1.0.0-beta.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.
Files changed (113) hide show
  1. package/.env.example +101 -0
  2. package/.feishu-codex-policy.example.json +11 -0
  3. package/.feishu-codex-runbooks.example.json +36 -0
  4. package/CHANGELOG.md +129 -0
  5. package/CODE_OF_CONDUCT.md +7 -0
  6. package/CONTRIBUTING.md +52 -0
  7. package/LICENSE +21 -0
  8. package/README.en.md +81 -0
  9. package/README.md +398 -0
  10. package/ROADMAP.md +40 -0
  11. package/SECURITY.md +53 -0
  12. package/dist/account-quota-card.js +233 -0
  13. package/dist/account-quota.js +125 -0
  14. package/dist/app-server-client.js +281 -0
  15. package/dist/card-session.js +166 -0
  16. package/dist/codex-events.js +1 -0
  17. package/dist/codex-runner.js +875 -0
  18. package/dist/config.js +198 -0
  19. package/dist/confirmation-card.js +135 -0
  20. package/dist/control-card.js +345 -0
  21. package/dist/conversation-turn-session.js +209 -0
  22. package/dist/data-maintenance.js +71 -0
  23. package/dist/device-card.js +460 -0
  24. package/dist/device-health.js +94 -0
  25. package/dist/diagnostics.js +253 -0
  26. package/dist/doctor.js +250 -0
  27. package/dist/fallback-card-session.js +37 -0
  28. package/dist/health-file.js +75 -0
  29. package/dist/index.js +4330 -0
  30. package/dist/lark-cli.js +558 -0
  31. package/dist/lark-retry.js +34 -0
  32. package/dist/maintenance.js +140 -0
  33. package/dist/model-capabilities.js +31 -0
  34. package/dist/onboarding-card.js +312 -0
  35. package/dist/permission-lease.js +22 -0
  36. package/dist/policy.js +506 -0
  37. package/dist/progress.js +267 -0
  38. package/dist/project-card.js +303 -0
  39. package/dist/project-overview-card.js +182 -0
  40. package/dist/project-overview.js +278 -0
  41. package/dist/project-policy.js +160 -0
  42. package/dist/project-registry.js +259 -0
  43. package/dist/project-status.js +45 -0
  44. package/dist/project-workspace.js +55 -0
  45. package/dist/quota-card.js +94 -0
  46. package/dist/recovery-policy.js +26 -0
  47. package/dist/redaction.js +67 -0
  48. package/dist/remote-ready.js +112 -0
  49. package/dist/response-card.js +139 -0
  50. package/dist/result-card.js +166 -0
  51. package/dist/review-card.js +452 -0
  52. package/dist/runbook-card.js +272 -0
  53. package/dist/runbooks.js +191 -0
  54. package/dist/runtime-card.js +337 -0
  55. package/dist/session-card.js +128 -0
  56. package/dist/session-naming.js +14 -0
  57. package/dist/smoke.js +28 -0
  58. package/dist/state-backup.js +302 -0
  59. package/dist/state-store.js +874 -0
  60. package/dist/task-card.js +640 -0
  61. package/dist/task-center-card.js +176 -0
  62. package/dist/task-failure.js +43 -0
  63. package/dist/task-intent.js +76 -0
  64. package/dist/task-queue.js +187 -0
  65. package/dist/task-reconciliation.js +80 -0
  66. package/dist/task-review.js +497 -0
  67. package/dist/team-card.js +275 -0
  68. package/dist/team-directory.js +54 -0
  69. package/dist/team-policy.js +93 -0
  70. package/dist/types.js +1 -0
  71. package/dist/version.js +9 -0
  72. package/dist/workspace-session.js +64 -0
  73. package/docs/ARCHITECTURE.md +54 -0
  74. package/docs/COMPATIBILITY.md +55 -0
  75. package/docs/CONFIGURATION.md +88 -0
  76. package/docs/DEMO.md +45 -0
  77. package/docs/GOOD_FIRST_ISSUES.md +23 -0
  78. package/docs/INSTALLATION.md +207 -0
  79. package/docs/OPEN_SOURCE_PRODUCT_PLAN.md +113 -0
  80. package/docs/PRODUCT_REQUIREMENTS_MAP.md +591 -0
  81. package/docs/RELEASE_CHECKLIST.md +65 -0
  82. package/docs/TEAM_DEPLOYMENT.md +35 -0
  83. package/docs/TROUBLESHOOTING.md +130 -0
  84. package/docs/V4_WORKSPACE_SESSION_FLOW.md +232 -0
  85. package/docs/requirements/D10_MAINTENANCE_AND_ECOSYSTEM.md +103 -0
  86. package/docs/requirements/D1_INSTALLATION_AND_FIRST_CONNECTION.md +479 -0
  87. package/docs/requirements/D2_DEVICE_AND_CONNECTIVITY.md +54 -0
  88. package/docs/requirements/D3_PROJECTS_AND_SESSIONS.md +107 -0
  89. package/docs/requirements/D4_REMOTE_TASK_EXECUTION.md +102 -0
  90. package/docs/requirements/D5_CODEX_NATIVE_INTERACTIONS.md +99 -0
  91. package/docs/requirements/D6_RESULTS_AND_CODE_REVIEW.md +100 -0
  92. package/docs/requirements/D7_SECURITY_GOVERNANCE.md +106 -0
  93. package/docs/requirements/D8_RELIABILITY_AND_RECOVERY.md +182 -0
  94. package/docs/requirements/D9_TEAM_COLLABORATION.md +129 -0
  95. package/package.json +76 -0
  96. package/scripts/capability-probe.mjs +113 -0
  97. package/scripts/cli.mjs +919 -0
  98. package/scripts/config-file.mjs +137 -0
  99. package/scripts/discovery-lib.mjs +78 -0
  100. package/scripts/install-card.mjs +37 -0
  101. package/scripts/install-detection.mjs +126 -0
  102. package/scripts/install-state.mjs +107 -0
  103. package/scripts/launchd.mjs +161 -0
  104. package/scripts/migrate-legacy.mjs +97 -0
  105. package/scripts/package-smoke.mjs +163 -0
  106. package/scripts/release-dist-tag.mjs +7 -0
  107. package/scripts/runbook-template.mjs +36 -0
  108. package/scripts/service-health.mjs +110 -0
  109. package/scripts/service.mjs +24 -0
  110. package/scripts/setup-lib.mjs +118 -0
  111. package/scripts/systemd.mjs +96 -0
  112. package/scripts/upgrade-lib.mjs +99 -0
  113. package/scripts/verify-release.mjs +37 -0
@@ -0,0 +1,259 @@
1
+ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ const SKIP_DIRECTORIES = new Set([
5
+ ".git",
6
+ ".idea",
7
+ ".vscode",
8
+ ".codex",
9
+ "node_modules",
10
+ "dist",
11
+ "build",
12
+ "coverage",
13
+ "vendor",
14
+ ]);
15
+ export class ProjectRegistry {
16
+ roots;
17
+ defaultWorkdir;
18
+ maxDepth;
19
+ maxProjects;
20
+ codexStateFile;
21
+ discovered = [];
22
+ canonicalRoots = [];
23
+ canonicalDefaultWorkdir;
24
+ canonicalHome = homedir();
25
+ constructor(roots, defaultWorkdir, maxDepth, maxProjects, codexStateFile) {
26
+ this.roots = roots;
27
+ this.defaultWorkdir = defaultWorkdir;
28
+ this.maxDepth = maxDepth;
29
+ this.maxProjects = maxProjects;
30
+ this.codexStateFile = codexStateFile;
31
+ this.canonicalDefaultWorkdir = defaultWorkdir;
32
+ }
33
+ async refresh() {
34
+ const projects = new Map();
35
+ try {
36
+ this.canonicalHome = await realpath(homedir());
37
+ }
38
+ catch {
39
+ this.canonicalHome = homedir();
40
+ }
41
+ if (this.codexStateFile) {
42
+ const savedProjects = await this.readCodexSavedProjects();
43
+ for (const saved of savedProjects) {
44
+ const project = await this.projectFromPath(saved.path, saved.name, "codex", false);
45
+ if (project && !projects.has(project.path))
46
+ projects.set(project.path, project);
47
+ }
48
+ }
49
+ const scannedPaths = new Set();
50
+ this.canonicalRoots = [];
51
+ for (const root of this.roots) {
52
+ let canonicalRoot;
53
+ try {
54
+ canonicalRoot = await realpath(root);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ this.canonicalRoots.push(canonicalRoot);
60
+ await this.scan(canonicalRoot, canonicalRoot, 0, scannedPaths);
61
+ if (scannedPaths.size >= this.maxProjects)
62
+ break;
63
+ }
64
+ for (const projectPath of [...scannedPaths].sort((left, right) => left.localeCompare(right))) {
65
+ const project = await this.projectFromPath(projectPath, undefined, "scan", true);
66
+ if (project && !projects.has(project.path))
67
+ projects.set(project.path, project);
68
+ }
69
+ const defaultProject = await this.projectFromPath(this.defaultWorkdir, undefined, "default", false);
70
+ if (!defaultProject) {
71
+ throw new Error(`Default Codex project is unavailable: ${this.defaultWorkdir}`);
72
+ }
73
+ this.canonicalDefaultWorkdir = defaultProject.path;
74
+ if (!projects.has(defaultProject.path))
75
+ projects.set(defaultProject.path, defaultProject);
76
+ const limited = [...projects.values()].slice(0, this.maxProjects);
77
+ if (!limited.some((project) => project.path === defaultProject.path)) {
78
+ limited[Math.max(0, limited.length - 1)] = defaultProject;
79
+ }
80
+ this.discovered = limited;
81
+ return limited;
82
+ }
83
+ list() {
84
+ return this.discovered;
85
+ }
86
+ defaultProject() {
87
+ return (this.getByPath(this.canonicalDefaultWorkdir) ?? {
88
+ name: path.basename(this.canonicalDefaultWorkdir),
89
+ path: this.canonicalDefaultWorkdir,
90
+ displayPath: this.displayPath(this.canonicalDefaultWorkdir),
91
+ isGitRepository: false,
92
+ source: "default",
93
+ });
94
+ }
95
+ getByPath(projectPath) {
96
+ return this.discovered.find((project) => project.path === projectPath);
97
+ }
98
+ resolve(selector) {
99
+ const query = selector.trim();
100
+ if (!query)
101
+ return { status: "missing" };
102
+ if (/^\d+$/.test(query)) {
103
+ const index = Number.parseInt(query, 10) - 1;
104
+ const project = this.discovered[index];
105
+ return project ? { status: "found", project } : { status: "missing" };
106
+ }
107
+ const normalized = query.toLocaleLowerCase();
108
+ const exact = this.discovered.filter((project) => project.name.toLocaleLowerCase() === normalized ||
109
+ project.displayPath.toLocaleLowerCase() === normalized ||
110
+ project.path.toLocaleLowerCase() === normalized);
111
+ if (exact.length === 1 && exact[0])
112
+ return { status: "found", project: exact[0] };
113
+ if (exact.length > 1)
114
+ return { status: "ambiguous", projects: exact };
115
+ const fuzzy = this.discovered.filter((project) => project.name.toLocaleLowerCase().includes(normalized) ||
116
+ project.displayPath.toLocaleLowerCase().includes(normalized));
117
+ if (fuzzy.length === 1 && fuzzy[0])
118
+ return { status: "found", project: fuzzy[0] };
119
+ if (fuzzy.length > 1)
120
+ return { status: "ambiguous", projects: fuzzy };
121
+ return { status: "missing" };
122
+ }
123
+ async scan(root, directory, depth, projects) {
124
+ if (projects.size >= this.maxProjects || depth > this.maxDepth)
125
+ return;
126
+ let canonical;
127
+ try {
128
+ canonical = await realpath(directory);
129
+ }
130
+ catch {
131
+ return;
132
+ }
133
+ if (!isWithin(root, canonical))
134
+ return;
135
+ // Additional roots are discovery-only, so arbitrary non-Git directories
136
+ // under them never become selectable. Non-Git folders are accepted only
137
+ // when they are exact projects explicitly saved in Codex (or the default).
138
+ if (await isGitRepository(canonical)) {
139
+ projects.add(canonical);
140
+ return;
141
+ }
142
+ let entries;
143
+ try {
144
+ entries = await readdir(canonical, { withFileTypes: true });
145
+ }
146
+ catch {
147
+ return;
148
+ }
149
+ for (const entry of entries) {
150
+ if (!entry.isDirectory() || entry.isSymbolicLink() || SKIP_DIRECTORIES.has(entry.name)) {
151
+ continue;
152
+ }
153
+ await this.scan(root, path.join(canonical, entry.name), depth + 1, projects);
154
+ if (projects.size >= this.maxProjects)
155
+ return;
156
+ }
157
+ }
158
+ displayPath(projectPath) {
159
+ for (const root of this.canonicalRoots) {
160
+ if (!isWithin(root, projectPath))
161
+ continue;
162
+ const relative = path.relative(root, projectPath);
163
+ return relative || path.basename(projectPath);
164
+ }
165
+ if (isWithin(this.canonicalHome, projectPath)) {
166
+ const relative = path.relative(this.canonicalHome, projectPath);
167
+ return relative ? `~/${relative}` : "~";
168
+ }
169
+ return projectPath;
170
+ }
171
+ async projectFromPath(projectPath, name, source, requireGit) {
172
+ let canonical;
173
+ try {
174
+ canonical = await realpath(projectPath);
175
+ const stats = await lstat(canonical);
176
+ if (!stats.isDirectory())
177
+ return null;
178
+ }
179
+ catch {
180
+ return null;
181
+ }
182
+ if (source === "codex" && isBroadDirectory(canonical, this.canonicalHome)) {
183
+ console.warn("[projects] ignored an overly broad Codex project entry");
184
+ return null;
185
+ }
186
+ const git = await isGitRepository(canonical);
187
+ if (requireGit && !git)
188
+ return null;
189
+ return {
190
+ name: name?.trim() || path.basename(canonical),
191
+ path: canonical,
192
+ displayPath: this.displayPath(canonical),
193
+ isGitRepository: git,
194
+ source,
195
+ };
196
+ }
197
+ async readCodexSavedProjects() {
198
+ if (!this.codexStateFile)
199
+ return [];
200
+ try {
201
+ const parsed = JSON.parse(await readFile(this.codexStateFile, "utf8"));
202
+ if (!isRecord(parsed))
203
+ return [];
204
+ const saved = stringArray(parsed["electron-saved-workspace-roots"]);
205
+ const order = stringArray(parsed["project-order"]);
206
+ const labels = isRecord(parsed["electron-workspace-root-labels"])
207
+ ? parsed["electron-workspace-root-labels"]
208
+ : {};
209
+ const savedSet = new Set(saved);
210
+ const ordered = [
211
+ ...saved.filter((projectPath) => !order.includes(projectPath)),
212
+ ...order.filter((projectPath) => savedSet.has(projectPath)),
213
+ ];
214
+ const seen = new Set();
215
+ return ordered.flatMap((projectPath) => {
216
+ if (seen.has(projectPath))
217
+ return [];
218
+ seen.add(projectPath);
219
+ const label = labels[projectPath];
220
+ return [
221
+ {
222
+ path: projectPath,
223
+ ...(typeof label === "string" && label.trim() ? { name: label.trim() } : {}),
224
+ },
225
+ ];
226
+ });
227
+ }
228
+ catch (error) {
229
+ if (error.code !== "ENOENT") {
230
+ console.warn(`[projects] unable to read Codex saved projects: ${error.message}`);
231
+ }
232
+ return [];
233
+ }
234
+ }
235
+ }
236
+ async function isGitRepository(directory) {
237
+ try {
238
+ await lstat(path.join(directory, ".git"));
239
+ return true;
240
+ }
241
+ catch {
242
+ return false;
243
+ }
244
+ }
245
+ function isWithin(root, candidate) {
246
+ const relative = path.relative(root, candidate);
247
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
248
+ }
249
+ function isRecord(value) {
250
+ return typeof value === "object" && value !== null && !Array.isArray(value);
251
+ }
252
+ function stringArray(value) {
253
+ if (!Array.isArray(value))
254
+ return [];
255
+ return value.filter((item) => typeof item === "string" && item.trim() !== "");
256
+ }
257
+ function isBroadDirectory(candidate, home) {
258
+ return candidate === path.parse(candidate).root || candidate === home;
259
+ }
@@ -0,0 +1,45 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export async function readProjectGitStatus(projectPath, run = runGitStatus) {
5
+ try {
6
+ return parseGitStatus(await run(projectPath));
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ export function parseGitStatus(output) {
13
+ const lines = output.split(/\r?\n/).filter(Boolean);
14
+ const header = lines[0]?.startsWith("## ") ? lines[0].slice(3) : "HEAD (unknown)";
15
+ const branchPart = (header.split("...")[0] ?? header).trim();
16
+ const branch = branchPart.replace(/^No commits yet on\s+/i, "").trim() || "HEAD";
17
+ const detached = /^HEAD(?:\s|$|\()/i.test(branch);
18
+ const ahead = numberFrom(header, /ahead\s+(\d+)/i);
19
+ const behind = numberFrom(header, /behind\s+(\d+)/i);
20
+ const changedFiles = lines[0]?.startsWith("## ") ? Math.max(0, lines.length - 1) : lines.length;
21
+ return {
22
+ branch,
23
+ detached,
24
+ dirty: changedFiles > 0,
25
+ changedFiles,
26
+ ahead,
27
+ behind,
28
+ };
29
+ }
30
+ async function runGitStatus(projectPath) {
31
+ const { stdout } = await execFileAsync("git", ["-C", projectPath, "status", "--short", "--branch", "--untracked-files=normal"], {
32
+ timeout: 3_000,
33
+ maxBuffer: 512 * 1024,
34
+ env: {
35
+ ...process.env,
36
+ GIT_OPTIONAL_LOCKS: "0",
37
+ GIT_TERMINAL_PROMPT: "0",
38
+ },
39
+ });
40
+ return stdout;
41
+ }
42
+ function numberFrom(value, pattern) {
43
+ const match = value.match(pattern);
44
+ return match?.[1] ? Number.parseInt(match[1], 10) : 0;
45
+ }
@@ -0,0 +1,55 @@
1
+ export function rankProjectWorkspace(projects, usage, recentLimit = 8) {
2
+ const visiblePaths = new Set(projects.map((project) => project.path));
3
+ const usageByPath = new Map(usage
4
+ .filter((entry) => visiblePaths.has(entry.projectPath))
5
+ .map((entry) => [entry.projectPath, entry]));
6
+ const favoritePaths = usage
7
+ .filter((entry) => entry.favorite && visiblePaths.has(entry.projectPath))
8
+ .sort(compareUsage)
9
+ .map((entry) => entry.projectPath);
10
+ const recentPaths = usage
11
+ .filter((entry) => entry.lastUsedAt && visiblePaths.has(entry.projectPath))
12
+ .sort(compareUsage)
13
+ .slice(0, recentLimit)
14
+ .map((entry) => entry.projectPath);
15
+ const projectsSorted = [...projects].sort((left, right) => {
16
+ const leftUsage = usageByPath.get(left.path);
17
+ const rightUsage = usageByPath.get(right.path);
18
+ const favoriteDifference = Number(rightUsage?.favorite ?? false) - Number(leftUsage?.favorite ?? false);
19
+ if (favoriteDifference !== 0)
20
+ return favoriteDifference;
21
+ const recencyDifference = (rightUsage?.lastUsedAt ?? "").localeCompare(leftUsage?.lastUsedAt ?? "");
22
+ if (recencyDifference !== 0)
23
+ return recencyDifference;
24
+ const nameDifference = left.name.localeCompare(right.name);
25
+ return nameDifference !== 0 ? nameDifference : left.path.localeCompare(right.path);
26
+ });
27
+ return { projects: projectsSorted, favoritePaths, recentPaths };
28
+ }
29
+ export function resolveProjectSelector(projects, selector) {
30
+ const query = selector.trim();
31
+ if (!query)
32
+ return { status: "missing" };
33
+ if (/^\d+$/.test(query)) {
34
+ const project = projects[Number.parseInt(query, 10) - 1];
35
+ return project ? { status: "found", project } : { status: "missing" };
36
+ }
37
+ const normalized = query.toLocaleLowerCase();
38
+ const exact = projects.filter((project) => project.name.toLocaleLowerCase() === normalized ||
39
+ project.displayPath.toLocaleLowerCase() === normalized ||
40
+ project.path.toLocaleLowerCase() === normalized);
41
+ if (exact.length === 1 && exact[0])
42
+ return { status: "found", project: exact[0] };
43
+ if (exact.length > 1)
44
+ return { status: "ambiguous", projects: exact };
45
+ const fuzzy = projects.filter((project) => project.name.toLocaleLowerCase().includes(normalized) ||
46
+ project.displayPath.toLocaleLowerCase().includes(normalized));
47
+ if (fuzzy.length === 1 && fuzzy[0])
48
+ return { status: "found", project: fuzzy[0] };
49
+ if (fuzzy.length > 1)
50
+ return { status: "ambiguous", projects: fuzzy };
51
+ return { status: "missing" };
52
+ }
53
+ function compareUsage(left, right) {
54
+ return (right.lastUsedAt ?? "").localeCompare(left.lastUsedAt ?? "");
55
+ }
@@ -0,0 +1,94 @@
1
+ import { renderAccountQuotaPanel } from "./account-quota-card.js";
2
+ export function renderQuotaCard(snapshot) {
3
+ return {
4
+ schema: "2.0",
5
+ config: {
6
+ update_multi: true,
7
+ width_mode: "default",
8
+ enable_forward: false,
9
+ summary: { content: `Codex 账户额度 · ${safe(snapshot.deviceName)}` },
10
+ },
11
+ header: {
12
+ title: { tag: "plain_text", content: "Codex 账户额度" },
13
+ subtitle: { tag: "plain_text", content: safe(snapshot.deviceName) },
14
+ template: snapshot.quota.status === "available" ? "turquoise" : "grey",
15
+ icon: { tag: "standard_icon", token: "myai_colorful" },
16
+ text_tag_list: [
17
+ {
18
+ tag: "text_tag",
19
+ text: {
20
+ tag: "plain_text",
21
+ content: snapshot.quota.status === "available" ? "实时读取" : "暂不可用",
22
+ },
23
+ color: snapshot.quota.status === "available" ? "green" : "neutral",
24
+ },
25
+ ],
26
+ },
27
+ body: {
28
+ direction: "vertical",
29
+ padding: "12px 12px 18px 12px",
30
+ vertical_spacing: "12px",
31
+ elements: [
32
+ ...(snapshot.feedback
33
+ ? [
34
+ {
35
+ tag: "markdown",
36
+ element_id: "quota_feedback",
37
+ text_size: "notation",
38
+ content: `<font color='blue'>${safe(snapshot.feedback)}</font>`,
39
+ },
40
+ ]
41
+ : []),
42
+ renderAccountQuotaPanel(snapshot.quota, "qq"),
43
+ {
44
+ tag: "column_set",
45
+ element_id: "quota_actions",
46
+ flex_mode: "none",
47
+ horizontal_spacing: "8px",
48
+ columns: [
49
+ actionColumn("quota_refresh_btn", "刷新额度", "primary_filled", "quota_refresh"),
50
+ actionColumn("quota_device_btn", "返回控制台", "default", "quota_device"),
51
+ ],
52
+ },
53
+ {
54
+ tag: "markdown",
55
+ element_id: "quota_note",
56
+ text_size: "notation",
57
+ text_align: "center",
58
+ content: "<font color='grey'>数据来自这台设备当前登录的 Codex 账户;不同模型可能使用独立额度窗口。</font>",
59
+ },
60
+ ],
61
+ },
62
+ };
63
+ }
64
+ function actionColumn(elementId, label, type, action) {
65
+ return {
66
+ tag: "column",
67
+ width: "weighted",
68
+ weight: 1,
69
+ elements: [
70
+ {
71
+ tag: "button",
72
+ element_id: elementId,
73
+ text: { tag: "plain_text", content: label },
74
+ type,
75
+ width: "fill",
76
+ behaviors: [
77
+ {
78
+ type: "callback",
79
+ value: { bridge: "feishu-codex-v4", action },
80
+ },
81
+ ],
82
+ },
83
+ ],
84
+ };
85
+ }
86
+ function safe(value) {
87
+ return value
88
+ .replaceAll("&", "&")
89
+ .replaceAll("<", "<")
90
+ .replaceAll(">", ">")
91
+ .replaceAll("*", "*")
92
+ .replaceAll("_", "_")
93
+ .replaceAll("`", "`");
94
+ }
@@ -0,0 +1,26 @@
1
+ export function assessBridgeRecovery(previous, { currentPid, now = Date.now(), processAlive, liveHeartbeatMs = 45_000, notifyAfterMs = 30_000, }) {
2
+ if (!previous) {
3
+ return { blockDuplicate: false, unexpected: false, offlineForMs: 0, notify: false };
4
+ }
5
+ const heartbeat = Date.parse(previous.updatedAt);
6
+ const offlineForMs = Number.isFinite(heartbeat) ? Math.max(0, now - heartbeat) : 0;
7
+ const validPid = Number.isSafeInteger(previous.pid) && previous.pid > 0;
8
+ const blockDuplicate = Boolean(validPid &&
9
+ previous.pid !== currentPid &&
10
+ offlineForMs < liveHeartbeatMs &&
11
+ processAlive(previous.pid));
12
+ const unexpected = !blockDuplicate && ["ready", "degraded", "failed"].includes(previous.status);
13
+ return {
14
+ blockDuplicate,
15
+ unexpected,
16
+ offlineForMs,
17
+ notify: unexpected && offlineForMs >= notifyAfterMs,
18
+ previousHeartbeatAt: previous.updatedAt,
19
+ };
20
+ }
21
+ export function shouldSendRecoveryNotice(lastNotice, now = Date.now(), minimumIntervalMs = 15 * 60_000) {
22
+ if (!lastNotice)
23
+ return true;
24
+ const previous = Date.parse(lastNotice);
25
+ return !Number.isFinite(previous) || now - previous >= minimumIntervalMs;
26
+ }
@@ -0,0 +1,67 @@
1
+ import path from "node:path";
2
+ const SECRET_NAME = "(?:app|client)[_-]?secret|access[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|password|passwd|private[_-]?key|authorization|cookie|session[_-]?(?:id|token|secret)|webhook[_-]?url";
3
+ export function redactSensitiveText(value, maxChars = Number.POSITIVE_INFINITY) {
4
+ const redacted = value
5
+ .replace(/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g, "[REDACTED PRIVATE KEY]")
6
+ .replace(/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, "Bearer [REDACTED]")
7
+ .replace(/\bBasic\s+[A-Za-z0-9+/=]{8,}/gi, "Basic [REDACTED]")
8
+ .replace(/\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{12,})\b/g, "[REDACTED TOKEN]")
9
+ .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, "[REDACTED JWT]")
10
+ .replace(new RegExp(`((?:"?(?:${SECRET_NAME})"?)\\s*[:=]\\s*)(?:"[^"]*"|'[^']*'|[^\\s,;]+)`, "gi"), "$1[REDACTED]")
11
+ .replace(/\b(https?:\/\/)([^\s/:@]+):([^\s/@]+)@/gi, "$1[REDACTED]:[REDACTED]@");
12
+ return redacted.slice(0, Math.max(0, maxChars));
13
+ }
14
+ export function prepareRemoteMarkdown(value, maxChars = Number.POSITIVE_INFINITY) {
15
+ const redacted = redactSensitiveText(value);
16
+ const withoutLocalLinks = redacted.replace(/\[([^\]\r\n]+)\]\(\s*<?(?:(?:file|vscode):\/{2,}|\/(?:Users|home|private|var|tmp)\/|~\/|[A-Za-z]:[\\/])[^)\r\n>]*>?\s*\)/g, (_match, label) => `\`${label.replaceAll("`", "")}\``);
17
+ return withoutLocalLinks.slice(0, Math.max(0, maxChars));
18
+ }
19
+ export function redactDiagnosticText(value, options = {}) {
20
+ let redacted = redactSensitiveText(value);
21
+ if (options.homeDirectory) {
22
+ const home = path.resolve(options.homeDirectory);
23
+ redacted = redacted.split(home).join("~");
24
+ }
25
+ return redacted.slice(0, options.maxChars ?? Number.POSITIVE_INFINITY);
26
+ }
27
+ export function safeErrorText(error, maxChars = 2_000) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return redactSensitiveText(message.replace(/[\r\n]+/g, " "), maxChars);
30
+ }
31
+ let consoleRedactionInstalled = false;
32
+ export function installConsoleRedaction(target = console) {
33
+ if (consoleRedactionInstalled)
34
+ return;
35
+ consoleRedactionInstalled = true;
36
+ for (const level of ["debug", "info", "warn", "error"]) {
37
+ const original = target[level].bind(target);
38
+ target[level] = ((...args) => {
39
+ original(...args.map((value) => redactLogValue(value)));
40
+ });
41
+ }
42
+ }
43
+ function redactLogValue(value, seen = new WeakSet()) {
44
+ if (typeof value === "string")
45
+ return redactSensitiveText(value, 8_000);
46
+ if (value instanceof Error) {
47
+ const stack = value.stack ? redactSensitiveText(value.stack, 8_000) : "";
48
+ return stack || `${value.name}: ${safeErrorText(value)}`;
49
+ }
50
+ if (Buffer.isBuffer(value))
51
+ return `[Buffer ${value.length} bytes]`;
52
+ if (typeof value === "object" && value !== null) {
53
+ if (seen.has(value))
54
+ return "[Circular]";
55
+ seen.add(value);
56
+ }
57
+ if (Array.isArray(value)) {
58
+ return value.slice(0, 50).map((entry) => redactLogValue(entry, seen));
59
+ }
60
+ if (typeof value === "object" && value !== null) {
61
+ const entries = Object.entries(value)
62
+ .slice(0, 50)
63
+ .map(([key, entry]) => [key, redactLogValue(entry, seen)]);
64
+ return Object.fromEntries(entries);
65
+ }
66
+ return value;
67
+ }
@@ -0,0 +1,112 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export class RemoteReadyController {
5
+ child = null;
6
+ enabled = false;
7
+ lastError;
8
+ get supported() {
9
+ return process.platform === "darwin";
10
+ }
11
+ async setEnabled(enabled) {
12
+ this.enabled = enabled;
13
+ if (!enabled) {
14
+ this.stopAssertion();
15
+ return this.getStatus();
16
+ }
17
+ if (!this.supported) {
18
+ this.lastError = "Remote Ready 当前仅支持 macOS caffeinate";
19
+ return this.getStatus();
20
+ }
21
+ if (this.child && this.child.exitCode === null && !this.child.killed)
22
+ return this.getStatus();
23
+ await new Promise((resolve, reject) => {
24
+ const child = execCaffeinate();
25
+ this.child = child;
26
+ const onError = (error) => {
27
+ if (this.child === child)
28
+ this.child = null;
29
+ this.lastError = error.message;
30
+ reject(error);
31
+ };
32
+ child.once("error", onError);
33
+ child.once("spawn", () => {
34
+ child.off("error", onError);
35
+ delete this.lastError;
36
+ resolve();
37
+ });
38
+ child.once("exit", (code, signal) => {
39
+ if (this.child !== child)
40
+ return;
41
+ this.child = null;
42
+ if (this.enabled && code !== 0) {
43
+ this.lastError = `caffeinate 已退出(${signal ?? code ?? "unknown"})`;
44
+ }
45
+ });
46
+ });
47
+ return this.getStatus();
48
+ }
49
+ getStatus() {
50
+ const active = Boolean(this.child && this.child.exitCode === null && !this.child.killed);
51
+ return {
52
+ supported: this.supported,
53
+ enabled: this.enabled,
54
+ active,
55
+ ...(active && this.child?.pid !== undefined ? { pid: this.child.pid } : {}),
56
+ ...(this.lastError ? { lastError: this.lastError } : {}),
57
+ };
58
+ }
59
+ close() {
60
+ this.enabled = false;
61
+ this.stopAssertion();
62
+ }
63
+ stopAssertion() {
64
+ const child = this.child;
65
+ this.child = null;
66
+ if (child && child.exitCode === null && !child.killed)
67
+ child.kill("SIGTERM");
68
+ }
69
+ }
70
+ export async function readPowerStatus() {
71
+ if (process.platform !== "darwin") {
72
+ return { source: "unknown", label: "电源状态不可用" };
73
+ }
74
+ try {
75
+ const { stdout } = await execFileAsync("/usr/bin/pmset", ["-g", "batt"], {
76
+ timeout: 3_000,
77
+ });
78
+ return parsePowerStatus(stdout);
79
+ }
80
+ catch {
81
+ return { source: "unknown", label: "电源状态不可用" };
82
+ }
83
+ }
84
+ export function parsePowerStatus(output) {
85
+ const source = /AC Power/i.test(output)
86
+ ? "ac"
87
+ : /Battery Power/i.test(output)
88
+ ? "battery"
89
+ : "unknown";
90
+ const percentMatch = output.match(/(\d{1,3})%/);
91
+ const rawPercent = percentMatch?.[1]
92
+ ? Number.parseInt(percentMatch[1], 10)
93
+ : undefined;
94
+ const percent = rawPercent !== undefined
95
+ ? Math.max(0, Math.min(100, rawPercent))
96
+ : undefined;
97
+ return {
98
+ source,
99
+ ...(percent !== undefined ? { percent } : {}),
100
+ label: source === "ac"
101
+ ? `外接电源${percent !== undefined ? ` · ${percent}%` : ""}`
102
+ : source === "battery"
103
+ ? `电池供电${percent !== undefined ? ` · ${percent}%` : ""}`
104
+ : "电源状态未知",
105
+ };
106
+ }
107
+ function execCaffeinate() {
108
+ return spawn("/usr/bin/caffeinate", ["-i", "-w", String(process.pid)], {
109
+ shell: false,
110
+ stdio: "ignore",
111
+ });
112
+ }