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,558 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { chmod, lstat, mkdir, realpath } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import readline from "node:readline";
6
+ import { isTransientLarkFailure, larkRetryDelayMs } from "./lark-retry.js";
7
+ import { redactSensitiveText } from "./redaction.js";
8
+ function childEnvironment() {
9
+ return {
10
+ ...process.env,
11
+ LARKSUITE_CLI_NO_UPDATE_NOTIFIER: "1",
12
+ LARKSUITE_CLI_NO_SKILLS_NOTIFIER: "1",
13
+ };
14
+ }
15
+ function truncate(text, maxChars) {
16
+ if (text.length <= maxChars)
17
+ return text;
18
+ return `${text.slice(0, Math.max(0, maxChars - 35))}\n\n[回复过长,已在此处截断]`;
19
+ }
20
+ function safeIdempotencyKey(value) {
21
+ return value.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 50);
22
+ }
23
+ export class LarkCli {
24
+ options;
25
+ consumerHealth = new Map();
26
+ apiHealth = { state: "idle", consecutiveFailures: 0 };
27
+ constructor(options) {
28
+ this.options = options;
29
+ }
30
+ getConsumerHealth() {
31
+ return [...this.consumerHealth.values()].map((health) => ({ ...health }));
32
+ }
33
+ getApiHealth() {
34
+ return { ...this.apiHealth };
35
+ }
36
+ async reply(messageId, text, idempotencyKey) {
37
+ const body = truncate(text, this.options.maxReplyChars);
38
+ await this.run([
39
+ "im",
40
+ "+messages-reply",
41
+ "--message-id",
42
+ messageId,
43
+ "--text",
44
+ body,
45
+ "--as",
46
+ "bot",
47
+ "--idempotency-key",
48
+ safeIdempotencyKey(idempotencyKey),
49
+ ]);
50
+ }
51
+ async replyMarkdown(messageId, markdown, idempotencyKey, replyInThread = false) {
52
+ const args = [
53
+ "im",
54
+ "+messages-reply",
55
+ "--message-id",
56
+ messageId,
57
+ "--markdown",
58
+ truncate(markdown, this.options.maxReplyChars),
59
+ "--as",
60
+ "bot",
61
+ "--idempotency-key",
62
+ safeIdempotencyKey(idempotencyKey),
63
+ "--format",
64
+ "json",
65
+ ];
66
+ if (replyInThread)
67
+ args.push("--reply-in-thread");
68
+ const response = await this.runJson(args);
69
+ return findString(response, "message_id");
70
+ }
71
+ async updateMarkdown(messageId, markdown) {
72
+ const body = truncate(markdown, this.options.maxReplyChars);
73
+ await this.run([
74
+ "api",
75
+ "PUT",
76
+ `/open-apis/im/v1/messages/${encodeURIComponent(messageId)}`,
77
+ "--as",
78
+ "bot",
79
+ "--data",
80
+ JSON.stringify({
81
+ msg_type: "post",
82
+ content: markdownPostContent(body),
83
+ }),
84
+ "--format",
85
+ "json",
86
+ ]);
87
+ }
88
+ async createCard(card) {
89
+ const response = await this.runJson([
90
+ "api",
91
+ "POST",
92
+ "/open-apis/cardkit/v1/cards",
93
+ "--as",
94
+ "bot",
95
+ "--data",
96
+ JSON.stringify({ type: "card_json", data: JSON.stringify(card) }),
97
+ "--format",
98
+ "json",
99
+ ]);
100
+ const cardId = findString(response, "card_id");
101
+ if (!cardId)
102
+ throw new Error("CardKit create response did not include card_id");
103
+ return cardId;
104
+ }
105
+ async replyCard(messageId, cardId, idempotencyKey, replyInThread = false) {
106
+ const args = [
107
+ "im",
108
+ "+messages-reply",
109
+ "--message-id",
110
+ messageId,
111
+ "--msg-type",
112
+ "interactive",
113
+ "--content",
114
+ JSON.stringify({ type: "card", data: { card_id: cardId } }),
115
+ "--as",
116
+ "bot",
117
+ "--idempotency-key",
118
+ safeIdempotencyKey(idempotencyKey),
119
+ "--format",
120
+ "json",
121
+ ];
122
+ if (replyInThread)
123
+ args.push("--reply-in-thread");
124
+ const response = await this.runJson(args);
125
+ return findString(response, "message_id");
126
+ }
127
+ async streamCardContent(cardId, elementId, content, sequence) {
128
+ await this.run([
129
+ "api",
130
+ "PUT",
131
+ `/open-apis/cardkit/v1/cards/${encodeURIComponent(cardId)}/elements/${encodeURIComponent(elementId)}/content`,
132
+ "--as",
133
+ "bot",
134
+ "--data",
135
+ JSON.stringify({ uuid: randomUUID(), content, sequence }),
136
+ "--format",
137
+ "json",
138
+ ]);
139
+ }
140
+ async updateCard(cardId, card, sequence) {
141
+ await this.run([
142
+ "api",
143
+ "PUT",
144
+ `/open-apis/cardkit/v1/cards/${encodeURIComponent(cardId)}`,
145
+ "--as",
146
+ "bot",
147
+ "--data",
148
+ JSON.stringify({
149
+ card: { type: "card_json", data: JSON.stringify(card) },
150
+ uuid: randomUUID(),
151
+ sequence,
152
+ }),
153
+ "--format",
154
+ "json",
155
+ ]);
156
+ }
157
+ async downloadMessageResources(messageId) {
158
+ const resourceRoot = path.resolve(this.options.cwd, "lark-im-resources");
159
+ await ensurePrivateDirectory(resourceRoot);
160
+ const response = await this.runJson([
161
+ "im",
162
+ "+messages-mget",
163
+ "--message-ids",
164
+ messageId,
165
+ "--no-reactions",
166
+ "--download-resources",
167
+ "--as",
168
+ "bot",
169
+ "--format",
170
+ "json",
171
+ ]);
172
+ const canonicalRoot = await realpath(resourceRoot);
173
+ const resources = [];
174
+ for (const resource of collectResources(response)) {
175
+ if (resource.error || !resource.local_path)
176
+ continue;
177
+ const localPath = path.resolve(this.options.cwd, resource.local_path);
178
+ if (!isWithin(resourceRoot, localPath)) {
179
+ console.warn(`[lark] ignored resource outside download directory key=${logRef(resource.key)}`);
180
+ continue;
181
+ }
182
+ try {
183
+ const canonicalPath = await realpath(localPath);
184
+ if (!isWithin(canonicalRoot, canonicalPath)) {
185
+ console.warn(`[lark] ignored resource resolving outside download directory key=${logRef(resource.key)}`);
186
+ continue;
187
+ }
188
+ const details = await lstat(canonicalPath);
189
+ if (!details.isFile())
190
+ continue;
191
+ await chmod(canonicalPath, 0o600);
192
+ resources.push({
193
+ key: resource.key,
194
+ type: resource.type,
195
+ localPath: canonicalPath,
196
+ sizeBytes: details.size,
197
+ });
198
+ }
199
+ catch (error) {
200
+ console.warn(`[lark] downloaded resource is unavailable key=${logRef(resource.key)}`, error);
201
+ }
202
+ }
203
+ return resources;
204
+ }
205
+ async consumeMessages(onEvent, signal, onReady) {
206
+ await this.consumeEvent("im.message.receive_v1", onEvent, signal, onReady);
207
+ }
208
+ async consumeEvent(eventKey, onEvent, signal, onReady) {
209
+ let retryMs = 1_000;
210
+ let hasReportedReady = false;
211
+ const health = { eventKey, ready: false, restartCount: 0 };
212
+ this.consumerHealth.set(eventKey, health);
213
+ while (!signal.aborted) {
214
+ const outcome = await this.consumeOnce(eventKey, onEvent, signal, () => {
215
+ retryMs = 1_000;
216
+ health.ready = true;
217
+ health.lastReadyAt = Date.now();
218
+ if (!hasReportedReady) {
219
+ hasReportedReady = true;
220
+ onReady?.();
221
+ }
222
+ });
223
+ if (signal.aborted)
224
+ break;
225
+ health.ready = false;
226
+ health.lastExitAt = Date.now();
227
+ health.restartCount += 1;
228
+ console.error(`[lark] event consumer ${eventKey} exited code=${outcome.code ?? "null"}; retrying in ${retryMs}ms`);
229
+ await wait(retryMs, signal);
230
+ retryMs = Math.min(retryMs * 2, 30_000);
231
+ }
232
+ }
233
+ consumeOnce(eventKey, onEvent, signal, onReady) {
234
+ return new Promise((resolve, reject) => {
235
+ const child = spawn(this.options.binary, ["event", "consume", eventKey, "--as", "bot"], {
236
+ cwd: this.options.cwd,
237
+ env: childEnvironment(),
238
+ shell: false,
239
+ stdio: ["pipe", "pipe", "pipe"],
240
+ });
241
+ const stdout = readline.createInterface({ input: child.stdout });
242
+ const stderr = readline.createInterface({ input: child.stderr });
243
+ stdout.on("line", (line) => {
244
+ if (!line.trim())
245
+ return;
246
+ try {
247
+ const parsed = JSON.parse(line);
248
+ void onEvent(parsed).catch((error) => {
249
+ console.error("[lark] event handler failed", error);
250
+ });
251
+ }
252
+ catch (error) {
253
+ console.error("[lark] ignored malformed event JSON", error.message);
254
+ }
255
+ });
256
+ stderr.on("line", (line) => {
257
+ if (line.includes(`[event] ready event_key=${eventKey}`))
258
+ onReady();
259
+ console.error(`[lark-cli] ${redactCliOutput(line)}`);
260
+ });
261
+ let terminateTimer;
262
+ const abort = () => {
263
+ child.stdin.end();
264
+ terminateTimer = setTimeout(() => child.kill("SIGTERM"), 4_000);
265
+ terminateTimer.unref();
266
+ };
267
+ signal.addEventListener("abort", abort, { once: true });
268
+ child.once("error", (error) => {
269
+ signal.removeEventListener("abort", abort);
270
+ if (terminateTimer)
271
+ clearTimeout(terminateTimer);
272
+ reject(error);
273
+ });
274
+ child.once("exit", (code) => {
275
+ signal.removeEventListener("abort", abort);
276
+ if (terminateTimer)
277
+ clearTimeout(terminateTimer);
278
+ stdout.close();
279
+ stderr.close();
280
+ resolve({ code });
281
+ });
282
+ });
283
+ }
284
+ async run(args) {
285
+ const maxAttempts = Math.max(1, this.options.maxRequestAttempts ?? 3);
286
+ let lastError;
287
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
288
+ try {
289
+ const output = await this.runOnce(args);
290
+ this.apiHealth.state = "ready";
291
+ this.apiHealth.consecutiveFailures = 0;
292
+ this.apiHealth.lastSuccessAt = Date.now();
293
+ delete this.apiHealth.lastError;
294
+ return output;
295
+ }
296
+ catch (error) {
297
+ lastError = error;
298
+ this.apiHealth.state = "degraded";
299
+ this.apiHealth.consecutiveFailures += 1;
300
+ this.apiHealth.lastFailureAt = Date.now();
301
+ this.apiHealth.lastError = redactCliOutput(error instanceof Error ? error.message : String(error)).slice(0, 500);
302
+ if (attempt >= maxAttempts || !isTransientLarkFailure(error))
303
+ throw error;
304
+ const delayMs = larkRetryDelayMs(attempt, error);
305
+ console.warn(`[lark] transient API failure attempt=${attempt}/${maxAttempts}; retrying in ${delayMs}ms`);
306
+ await delay(delayMs);
307
+ }
308
+ }
309
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
310
+ }
311
+ runOnce(args) {
312
+ return new Promise((resolve, reject) => {
313
+ const child = spawn(this.options.binary, args, {
314
+ cwd: this.options.cwd,
315
+ env: childEnvironment(),
316
+ shell: false,
317
+ stdio: ["ignore", "pipe", "pipe"],
318
+ });
319
+ let stdout = "";
320
+ let stderr = "";
321
+ let timedOut = false;
322
+ let settled = false;
323
+ let forceKillTimer;
324
+ const timeout = setTimeout(() => {
325
+ timedOut = true;
326
+ child.kill("SIGTERM");
327
+ forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 2_000);
328
+ forceKillTimer.unref();
329
+ }, Math.max(1_000, this.options.requestTimeoutMs ?? 20_000));
330
+ timeout.unref();
331
+ const finish = (operation) => {
332
+ if (settled)
333
+ return;
334
+ settled = true;
335
+ clearTimeout(timeout);
336
+ if (forceKillTimer)
337
+ clearTimeout(forceKillTimer);
338
+ operation();
339
+ };
340
+ child.stdout.setEncoding("utf8");
341
+ child.stderr.setEncoding("utf8");
342
+ child.stdout.on("data", (chunk) => {
343
+ stdout += chunk;
344
+ });
345
+ child.stderr.on("data", (chunk) => {
346
+ stderr += chunk;
347
+ });
348
+ child.once("error", (error) => finish(() => reject(error)));
349
+ child.once("exit", (code) => {
350
+ if (timedOut) {
351
+ finish(() => reject(new Error(`lark-cli request timed out after ${Math.max(1_000, this.options.requestTimeoutMs ?? 20_000)}ms`)));
352
+ return;
353
+ }
354
+ if (code === 0) {
355
+ finish(() => resolve(stdout));
356
+ return;
357
+ }
358
+ finish(() => reject(new Error(`lark-cli exited with code ${code}: ${redactCliOutput(stderr.trim() || stdout.trim())}`)));
359
+ });
360
+ });
361
+ }
362
+ async runJson(args) {
363
+ const stdout = await this.run(args);
364
+ try {
365
+ return JSON.parse(stdout);
366
+ }
367
+ catch (error) {
368
+ throw new Error(`Unable to parse lark-cli JSON response: ${error.message}`);
369
+ }
370
+ }
371
+ }
372
+ export function markdownPostContent(markdown) {
373
+ const presentation = markdownPostPresentation(markdown);
374
+ return JSON.stringify({
375
+ zh_cn: {
376
+ ...(presentation.title ? { title: presentation.title } : {}),
377
+ content: [[{ tag: "md", text: presentation.body }]],
378
+ },
379
+ });
380
+ }
381
+ const NATIVE_POST_TITLES = new Set([
382
+ "一句话定位",
383
+ "结论",
384
+ "核心结论",
385
+ "简要回答",
386
+ "回答",
387
+ "建议",
388
+ "原因",
389
+ "结果",
390
+ "下一步",
391
+ "summary",
392
+ "answer",
393
+ "recommendation",
394
+ ]);
395
+ function markdownPostPresentation(markdown) {
396
+ const source = markdown.trim();
397
+ if (!source)
398
+ return { body: " " };
399
+ const newlineIndex = source.indexOf("\n");
400
+ const firstLine = (newlineIndex === -1 ? source : source.slice(0, newlineIndex)).trim();
401
+ const remainder = newlineIndex === -1
402
+ ? ""
403
+ : source.slice(newlineIndex + 1).replace(/^\s*\n/, "").trim();
404
+ const heading = firstLine.match(/^#{1,3}\s+(.+)$/);
405
+ if (heading && remainder) {
406
+ const title = cleanPostTitle(heading[1]);
407
+ if (title)
408
+ return { title, body: remainder };
409
+ }
410
+ const standaloneBold = firstLine.match(/^\*\*([^*]+)\*\*$/);
411
+ if (standaloneBold && remainder) {
412
+ const title = cleanPostTitle(standaloneBold[1]);
413
+ if (title && title.length <= 36)
414
+ return { title, body: remainder };
415
+ }
416
+ const emphasizedLabel = firstLine.match(/^\*\*([^*::]{1,24})[::]\*\*\s*(.+)$/);
417
+ if (emphasizedLabel && isNativePostTitle(emphasizedLabel[1])) {
418
+ const label = cleanPostTitle(emphasizedLabel[1]);
419
+ const lead = unwrapOuterBold(emphasizedLabel[2]);
420
+ return labelledPostPresentation(label, lead, remainder);
421
+ }
422
+ const labelledLead = firstLine.match(/^([^::]{1,24})[::]\s*(.+)$/);
423
+ if (labelledLead && isNativePostTitle(labelledLead[1])) {
424
+ const label = cleanPostTitle(labelledLead[1]);
425
+ const lead = unwrapOuterBold(labelledLead[2]);
426
+ return labelledPostPresentation(label, lead, remainder);
427
+ }
428
+ return { body: source };
429
+ }
430
+ function isNativePostTitle(value) {
431
+ return NATIVE_POST_TITLES.has(cleanPostTitle(value).toLowerCase());
432
+ }
433
+ function cleanPostTitle(value) {
434
+ return value
435
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
436
+ .replace(/[\*_`~>#]/g, "")
437
+ .replace(/\s+/g, " ")
438
+ .trim()
439
+ .slice(0, 60);
440
+ }
441
+ function unwrapOuterBold(value) {
442
+ const trimmed = value.trim();
443
+ const match = trimmed.match(/^\*\*([\s\S]+)\*\*([。!?!?.,,;;]?)$/);
444
+ return match ? `${match[1]}${match[2]}`.trim() : trimmed;
445
+ }
446
+ function labelledPostPresentation(label, lead, remainder) {
447
+ const conciseLead = cleanPostTitle(lead);
448
+ if (remainder && conciseLead.length >= 4 && conciseLead.length <= 60) {
449
+ return { title: conciseLead, body: remainder };
450
+ }
451
+ return {
452
+ title: label,
453
+ body: joinLeadAndRemainder(lead, remainder),
454
+ };
455
+ }
456
+ function joinLeadAndRemainder(lead, remainder) {
457
+ if (!remainder)
458
+ return lead || " ";
459
+ if (!lead)
460
+ return remainder;
461
+ return `${lead}\n\n${remainder}`;
462
+ }
463
+ async function ensurePrivateDirectory(directory) {
464
+ try {
465
+ const details = await lstat(directory);
466
+ if (!details.isDirectory() || details.isSymbolicLink()) {
467
+ throw new Error(`Unsafe resource directory: ${directory}`);
468
+ }
469
+ }
470
+ catch (error) {
471
+ if (error.code !== "ENOENT")
472
+ throw error;
473
+ await mkdir(directory, { recursive: true, mode: 0o700 });
474
+ }
475
+ await chmod(directory, 0o700);
476
+ }
477
+ function collectResources(value, result = []) {
478
+ if (Array.isArray(value)) {
479
+ for (const entry of value)
480
+ collectResources(entry, result);
481
+ return result;
482
+ }
483
+ if (typeof value !== "object" || value === null)
484
+ return result;
485
+ const record = value;
486
+ if (Array.isArray(record.resources)) {
487
+ for (const entry of record.resources) {
488
+ if (typeof entry !== "object" || entry === null)
489
+ continue;
490
+ const candidate = entry;
491
+ if (typeof candidate.key === "string" &&
492
+ (candidate.type === "image" || candidate.type === "file")) {
493
+ result.push({
494
+ key: candidate.key,
495
+ type: candidate.type,
496
+ ...(typeof candidate.local_path === "string"
497
+ ? { local_path: candidate.local_path }
498
+ : {}),
499
+ ...(candidate.error === true ? { error: true } : {}),
500
+ });
501
+ }
502
+ }
503
+ }
504
+ for (const [key, entry] of Object.entries(record)) {
505
+ if (key !== "resources")
506
+ collectResources(entry, result);
507
+ }
508
+ return result;
509
+ }
510
+ function isWithin(root, candidate) {
511
+ const relative = path.relative(root, candidate);
512
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
513
+ }
514
+ function findString(value, key, depth = 0) {
515
+ if (depth > 6 || typeof value !== "object" || value === null)
516
+ return null;
517
+ if (!Array.isArray(value)) {
518
+ const record = value;
519
+ if (typeof record[key] === "string" && record[key])
520
+ return record[key];
521
+ for (const nested of Object.values(record)) {
522
+ const found = findString(nested, key, depth + 1);
523
+ if (found)
524
+ return found;
525
+ }
526
+ return null;
527
+ }
528
+ for (const nested of value) {
529
+ const found = findString(nested, key, depth + 1);
530
+ if (found)
531
+ return found;
532
+ }
533
+ return null;
534
+ }
535
+ async function wait(ms, signal) {
536
+ if (signal.aborted)
537
+ return;
538
+ await new Promise((resolve) => {
539
+ const timer = setTimeout(resolve, ms);
540
+ const abort = () => {
541
+ clearTimeout(timer);
542
+ resolve();
543
+ };
544
+ signal.addEventListener("abort", abort, { once: true });
545
+ });
546
+ }
547
+ function delay(ms) {
548
+ return new Promise((resolve) => setTimeout(resolve, ms));
549
+ }
550
+ export function eventSummary(event) {
551
+ return `event=${logRef(event.event_id)} chat=${logRef(event.chat_id)} sender=${logRef(event.sender_id)} type=${event.message_type}`;
552
+ }
553
+ export function logRef(value) {
554
+ return createHash("sha256").update(value).digest("hex").slice(0, 10);
555
+ }
556
+ function redactCliOutput(value) {
557
+ return redactSensitiveText(value, 4_000);
558
+ }
@@ -0,0 +1,34 @@
1
+ const TRANSIENT_PATTERNS = [
2
+ /\b429\b/,
3
+ /rate[ -]?limit/i,
4
+ /too many requests/i,
5
+ /retry[ -]?after/i,
6
+ /\b5\d\d\b/,
7
+ /timeout|timed out/i,
8
+ /econn(?:reset|refused|aborted)/i,
9
+ /enetunreach|ehostunreach|eai_again/i,
10
+ /socket hang up|network is unreachable/i,
11
+ /temporar(?:y|ily) unavailable/i,
12
+ ];
13
+ export function isTransientLarkFailure(error) {
14
+ const text = error instanceof Error ? error.message : String(error);
15
+ return TRANSIENT_PATTERNS.some((pattern) => pattern.test(text));
16
+ }
17
+ export function larkRetryDelayMs(failedAttempt, error, random = Math.random) {
18
+ const explicit = retryAfterMs(error);
19
+ if (explicit !== undefined)
20
+ return Math.min(Math.max(explicit, 250), 30_000);
21
+ const exponential = Math.min(750 * 2 ** Math.max(0, failedAttempt - 1), 8_000);
22
+ const jitter = Math.floor(exponential * 0.2 * Math.max(0, Math.min(1, random())));
23
+ return exponential + jitter;
24
+ }
25
+ export function retryAfterMs(error) {
26
+ const text = error instanceof Error ? error.message : String(error);
27
+ const milliseconds = text.match(/retry[- ]after\s*[:=]?\s*(\d+)\s*ms/i)?.[1];
28
+ if (milliseconds)
29
+ return Number.parseInt(milliseconds, 10);
30
+ const seconds = text.match(/retry[- ]after\s*[:=]?\s*(\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i)?.[1];
31
+ if (seconds)
32
+ return Math.ceil(Number.parseFloat(seconds) * 1_000);
33
+ return undefined;
34
+ }