lark-remote 0.5.4 → 0.5.5

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 (64) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/bridge/approval-coordinator.d.ts +18 -1
  5. package/dist/bridge/approval-coordinator.js +57 -10
  6. package/dist/bridge/index.js +90 -17
  7. package/dist/card/approval-render.js +18 -0
  8. package/dist/card/run-state.js +10 -5
  9. package/dist/config/index.d.ts +16 -0
  10. package/dist/config/index.js +46 -6
  11. package/dist/config/opencode-config.d.ts +3 -3
  12. package/dist/config/opencode-config.js +38 -22
  13. package/dist/inbound/authorize.d.ts +34 -0
  14. package/dist/inbound/authorize.js +31 -0
  15. package/dist/index.js +116 -63
  16. package/dist/instance-lock.d.ts +9 -0
  17. package/dist/instance-lock.js +113 -42
  18. package/dist/router/config/dsh.d.ts +51 -21
  19. package/dist/router/config/dsh.js +255 -66
  20. package/dist/router/config/opencode.js +16 -6
  21. package/dist/router/config/types.d.ts +6 -3
  22. package/dist/router/index.d.ts +1 -0
  23. package/dist/router/index.js +13 -5
  24. package/dist/runner/common/acp/base-acp-runner.d.ts +12 -0
  25. package/dist/runner/common/acp/base-acp-runner.js +83 -8
  26. package/dist/runner/common/acp/protocol-types.d.ts +10 -2
  27. package/dist/runner/common/acp/protocol-types.js +3 -1
  28. package/dist/runner/common/connection-based-runner.d.ts +15 -0
  29. package/dist/runner/common/connection-based-runner.js +40 -5
  30. package/dist/runner/common/jsonrpc/connection-manager.d.ts +15 -0
  31. package/dist/runner/common/jsonrpc/connection-manager.js +83 -50
  32. package/dist/runner/dsh/approval.d.ts +53 -0
  33. package/dist/runner/dsh/approval.js +161 -0
  34. package/dist/runner/dsh/auth.d.ts +36 -0
  35. package/dist/runner/dsh/auth.js +118 -0
  36. package/dist/runner/dsh/client.d.ts +161 -42
  37. package/dist/runner/dsh/client.js +551 -244
  38. package/dist/runner/dsh/daemon.d.ts +85 -0
  39. package/dist/runner/dsh/daemon.js +272 -0
  40. package/dist/runner/dsh/merge.d.ts +9 -0
  41. package/dist/runner/dsh/merge.js +51 -0
  42. package/dist/runner/dsh/runner.d.ts +94 -11
  43. package/dist/runner/dsh/runner.js +553 -67
  44. package/dist/runner/dsh/translator.d.ts +43 -3
  45. package/dist/runner/dsh/translator.js +115 -38
  46. package/dist/runner/dsh/types.d.ts +189 -27
  47. package/dist/runner/dsh/types.js +11 -5
  48. package/dist/runner/kimi/acp/runner.js +15 -3
  49. package/dist/runner/opencode/acp/error-monitor.d.ts +2 -2
  50. package/dist/runner/opencode/acp/error-monitor.js +2 -2
  51. package/dist/runner/opencode/acp/runner.d.ts +9 -0
  52. package/dist/runner/opencode/acp/runner.js +40 -1
  53. package/dist/runner/opencode/acp/translator.d.ts +4 -2
  54. package/dist/runner/opencode/acp/translator.js +12 -3
  55. package/dist/runner/pi/rpc/translator.d.ts +2 -0
  56. package/dist/runner/pi/rpc/translator.js +27 -1
  57. package/dist/runner/types.d.ts +34 -0
  58. package/dist/runner/types.js +24 -0
  59. package/dist/session/dsh/sessions.d.ts +45 -6
  60. package/dist/session/dsh/sessions.js +171 -27
  61. package/dist/session/opencode/sessions.d.ts +18 -7
  62. package/dist/session/opencode/sessions.js +294 -15
  63. package/dist/session/pi/sessions.js +6 -2
  64. package/package.json +3 -1
@@ -2,7 +2,7 @@
2
2
  * OpencodeLogErrorMonitor: watches opencode's own log file for LLM stream
3
3
  * errors of the active ACP session and reports them immediately.
4
4
  *
5
- * Why this exists: opencode ACP (v1.18.x) does NOT forward retry/error state
5
+ * Why this exists: opencode ACP (v1.18.x and v2.0.x) does NOT forward retry/error state
6
6
  * to the client over the ACP protocol. Its session/update notifications are
7
7
  * limited to content deltas (agent_message_chunk / agent_thought_chunk /
8
8
  * tool_call / tool_call_update), usage and available-commands updates — there
@@ -36,7 +36,7 @@ const ERROR_MESSAGE_RE = /error\.error="((?:[^"\\]|\\.)*)"/;
36
36
  * Resolve opencode's log file path from the XDG data dir
37
37
  * (`$XDG_DATA_HOME/opencode/log/opencode.log`, default
38
38
  * `~/.local/share/opencode/log/opencode.log`). Matches opencode's own
39
- * data-dir layout (open source v1.18.x: `opencode/src/cli/global.ts`).
39
+ * data-dir layout (verified against v1.18.x and v2.0.32 `debug paths`).
40
40
  */
41
41
  export function resolveOpencodeLogPath() {
42
42
  const dataHome = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
@@ -84,6 +84,15 @@ export declare class OpencodeAcpRunner extends BaseAcpRunner<OpencodeAcpTranslat
84
84
  */
85
85
  protected onCompactPromptFired(promptPromise: Promise<SessionPromptResult>, translator: OpencodeAcpTranslator, sessionId: string, _opts: unknown, _baselineTotalCount: number): void;
86
86
  protected buildApprovalOutcome(action: string, pending: AcpPendingApproval, _response: unknown): unknown;
87
+ /**
88
+ * opencode 2 is ACP client → shared background daemon. The daemon can be
89
+ * starting or still activating location plugins when the first session is
90
+ * created; both surface as a JSON-RPC internal error. Retry a few times
91
+ * before failing the turn.
92
+ */
93
+ protected sessionSetupRetryDelaysMs(): number[];
94
+ protected isRetryableSessionSetupError(err: unknown): boolean;
95
+ protected enrichSessionSetupError(err: unknown): Error;
87
96
  getStatusInfo(): AgentStatusInfo;
88
97
  /**
89
98
  * Hot-apply a mode change to the live ACP session.
@@ -56,6 +56,13 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
56
56
  terminal: false,
57
57
  },
58
58
  },
59
+ onConnected: (result) => {
60
+ const version = result?.agentInfo
61
+ ?.version;
62
+ if (version) {
63
+ getLogger().info(`[opencode-acp-runner] connected to OpenCode ${version}`);
64
+ }
65
+ },
59
66
  };
60
67
  super({
61
68
  kind: opts.kind,
@@ -144,11 +151,43 @@ export class OpencodeAcpRunner extends BaseAcpRunner {
144
151
  * translation (no wire.jsonl polling, unlike kimi).
145
152
  */
146
153
  onCompactPromptFired(promptPromise, translator, sessionId, _opts, _baselineTotalCount) {
147
- promptPromise.then((result) => this.handlePromptResolved(result, translator, sessionId), (err) => this.handlePromptRejected(err, translator, sessionId, 'compact'));
154
+ // 与 setupTurn 相同,捕获发起压缩那一轮的代际 + client;
155
+ // 旧轮迟到的 prompt 结算不得写入新轮的共享队列。
156
+ const generation = this.turnGeneration;
157
+ const client = this.currentClient;
158
+ promptPromise.then((result) => {
159
+ if (!this.isCurrentTurn(generation, client))
160
+ return;
161
+ this.handlePromptResolved(result, translator, sessionId);
162
+ }, (err) => {
163
+ if (!this.isCurrentTurn(generation, client))
164
+ return;
165
+ this.handlePromptRejected(err, translator, sessionId, 'compact');
166
+ });
148
167
  }
149
168
  buildApprovalOutcome(action, pending, _response) {
150
169
  return buildAcpPermissionOutcome(action, pending.options, OPENCODE_APPROVAL_KINDS);
151
170
  }
171
+ /**
172
+ * opencode 2 is ACP client → shared background daemon. The daemon can be
173
+ * starting or still activating location plugins when the first session is
174
+ * created; both surface as a JSON-RPC internal error. Retry a few times
175
+ * before failing the turn.
176
+ */
177
+ sessionSetupRetryDelaysMs() {
178
+ return [500, 1500, 3000];
179
+ }
180
+ isRetryableSessionSetupError(err) {
181
+ const message = err instanceof Error ? err.message : String(err);
182
+ return /ServiceUnavailableError|Location plugins did not finish activating|Internal service failure|ECONNREFUSED|fetch failed/i.test(message);
183
+ }
184
+ enrichSessionSetupError(err) {
185
+ if (!this.isRetryableSessionSetupError(err)) {
186
+ return super.enrichSessionSetupError(err);
187
+ }
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ return new Error(`opencode 2.0 后台服务不可用(${message})。请在终端执行 \`opencode service restart\` 后重试。`, { cause: err });
190
+ }
152
191
  getStatusInfo() {
153
192
  return {
154
193
  kind: this.kind,
@@ -9,8 +9,10 @@
9
9
  * `props.delta` verbatim), so text/thinking chunks go straight onto the
10
10
  * `assistant` incremental channel — NO accumulation, NO turn_diff snapshot.
11
11
  * (Contrast kimi: same field is also a delta but kimi's contract accumulates
12
- * into turn_diff snapshots; both end with exactly one concatenation in
13
- * run-state, enforced by each side's seam test.)
12
+ * into turn_diff snapshots. Both end as verbatim text in run-state: the
13
+ * assistant channel appends deltas sharing one `itemId`, the snapshot channel
14
+ * replaces by itemId. opencode's `agent_thought_chunk.messageId`
15
+ * (`<msg>:reasoning:<ordinal>`) IS that block identity — pass it through.)
14
16
  *
15
17
  * Event mapping (source: opencode acp/service.ts + event.ts + tool.ts,
16
18
  * dev@1c965451b5):
@@ -9,8 +9,10 @@
9
9
  * `props.delta` verbatim), so text/thinking chunks go straight onto the
10
10
  * `assistant` incremental channel — NO accumulation, NO turn_diff snapshot.
11
11
  * (Contrast kimi: same field is also a delta but kimi's contract accumulates
12
- * into turn_diff snapshots; both end with exactly one concatenation in
13
- * run-state, enforced by each side's seam test.)
12
+ * into turn_diff snapshots. Both end as verbatim text in run-state: the
13
+ * assistant channel appends deltas sharing one `itemId`, the snapshot channel
14
+ * replaces by itemId. opencode's `agent_thought_chunk.messageId`
15
+ * (`<msg>:reasoning:<ordinal>`) IS that block identity — pass it through.)
14
16
  *
15
17
  * Event mapping (source: opencode acp/service.ts + event.ts + tool.ts,
16
18
  * dev@1c965451b5):
@@ -54,7 +56,14 @@ export class OpencodeAcpTranslator extends BaseAcpTranslator {
54
56
  {
55
57
  type: 'assistant',
56
58
  message: {
57
- content: [{ type: 'thinking', thinking: event.content.text }],
59
+ content: [
60
+ {
61
+ type: 'thinking',
62
+ thinking: event.content.text,
63
+ // 同一 reasoning part 的续写 delta 共享 messageId;换 part 才开新块。
64
+ ...(event.messageId !== undefined ? { itemId: event.messageId } : {}),
65
+ },
66
+ ],
58
67
  },
59
68
  timestamp: new Date().toISOString(),
60
69
  },
@@ -34,6 +34,8 @@ export declare class PiRpcTranslator {
34
34
  private hasUsage;
35
35
  /** toolcall_start bookkeeping:占位不发事件,end 时补发完整 tool_use。 */
36
36
  private pendingToolCall;
37
+ /** 思考块序号:每条消息的每个 thinking 块一个稳定 itemId。 */
38
+ private thinkingSeq;
37
39
  /** Provider failure (stopReason="error") on the final assistant message. */
38
40
  private lastAssistantStopReason;
39
41
  private lastAssistantErrorMessage;
@@ -25,6 +25,8 @@ export class PiRpcTranslator {
25
25
  hasUsage = false;
26
26
  /** toolcall_start bookkeeping:占位不发事件,end 时补发完整 tool_use。 */
27
27
  pendingToolCall = false;
28
+ /** 思考块序号:每条消息的每个 thinking 块一个稳定 itemId。 */
29
+ thinkingSeq = 0;
28
30
  /** Provider failure (stopReason="error") on the final assistant message. */
29
31
  lastAssistantStopReason;
30
32
  lastAssistantErrorMessage;
@@ -192,9 +194,16 @@ export class PiRpcTranslator {
192
194
  this.lastAssistantErrorMessage = m.stopReason === 'error' ? m.errorMessage : undefined;
193
195
  if (m.usage)
194
196
  this.addUsage(m.usage);
197
+ // 块生产者职责:连续 thinking 块在这里合并并写入 '\n' 段落分隔,
198
+ // 再给每个块一个 itemId。run-state 只按 itemId 逐字追加,不猜分隔符。
199
+ const content = mergeConsecutiveThinking(this.content);
200
+ for (const block of content) {
201
+ if (block.type === 'thinking')
202
+ block.itemId = `pi:${++this.thinkingSeq}`;
203
+ }
195
204
  const event = {
196
205
  type: 'assistant',
197
- message: { content: this.content },
206
+ message: { content },
198
207
  timestamp: new Date().toISOString(),
199
208
  };
200
209
  this.content = [];
@@ -291,4 +300,21 @@ export class PiRpcTranslator {
291
300
  }
292
301
  }
293
302
  }
303
+ /**
304
+ * 合并同一消息内连续的 thinking 块(块间用 '\n'),保留其余内容顺序。
305
+ * 与 c0e1fb7 的“避免碎片化思考面板”决定一致。
306
+ */
307
+ function mergeConsecutiveThinking(content) {
308
+ const out = [];
309
+ for (const block of content) {
310
+ const last = out[out.length - 1];
311
+ if (block.type === 'thinking' && last?.type === 'thinking') {
312
+ out[out.length - 1] = { ...last, thinking: `${last.thinking}\n${block.thinking}` };
313
+ }
314
+ else {
315
+ out.push(block.type === 'thinking' ? { ...block } : block);
316
+ }
317
+ }
318
+ return out;
319
+ }
294
320
  //# sourceMappingURL=translator.js.map
@@ -148,6 +148,11 @@ export interface ApprovalView {
148
148
  requestId: number | string;
149
149
  kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
150
150
  reason?: string;
151
+ /**
152
+ * Agent-agnostic scope note rendered at the top of the approval area (e.g. DSH's
153
+ * "this grant applies to one call only"). Keep the audit reason untouched.
154
+ */
155
+ note?: string;
151
156
  /**
152
157
  * 提问卡顶部概要行(数据驱动,无 agent 分支):Kimi elicitation form 的
153
158
  * 题面只在 form 级 message 里合并出现,逐字段 title 可能只是短 header,
@@ -201,10 +206,24 @@ export interface ApprovalView {
201
206
  availableDecisions: string[];
202
207
  /** Raw payloads for structured decisions (e.g. acceptWithExecpolicyAmendment). */
203
208
  decisionPayloads?: Record<string, unknown>;
209
+ /**
210
+ * 最近一次响应发送失败的原因。协调器失败后回滚 pending 并写回
211
+ * 该字段,卡片据此渲染「提交失败,可重试」,不再让用户误以为已提交。
212
+ */
213
+ deliveryError?: string;
204
214
  }
205
215
  interface ThinkingContent {
206
216
  type: 'thinking';
207
217
  thinking: string;
218
+ /**
219
+ * 思考块身份。同一 itemId 的连续 `thinking` 内容是**同一块的增量**,run-state
220
+ * 逐字拼接(不插入任何分隔符);itemId 变化才开启新块。缺省表示"续写当前块"
221
+ * (块生产者必须显式给 id,避免与上一块粘连)。
222
+ *
223
+ * 来源:opencode `agent_thought_chunk.messageId`(`<msg>:reasoning:<ordinal>`)、
224
+ * DSH `reasoning-delta` 的 attemptId+index、pi 每条消息的块序号。
225
+ */
226
+ itemId?: string;
208
227
  }
209
228
  interface TextContent {
210
229
  type: 'text';
@@ -412,6 +431,21 @@ export interface Runner {
412
431
  */
413
432
  export declare const AGENT_KINDS: readonly ["claude", "codex", "opencode", "pi", "kimi", "dsh"];
414
433
  export type AgentKind = (typeof AGENT_KINDS)[number];
434
+ /**
435
+ * Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
436
+ *
437
+ * 键集就是 `AgentKind`:新增 agent 时缺一项 = TypeScript 报错,必须显式回答
438
+ * 「这个 agent 能不能压缩」。历史教训:能力只由 runner 上有没有 `runCompact`
439
+ * 这个鸭子方法隐式决定,dsh 接入时漏了实现,run 卡片直接不渲染按钮——没有
440
+ * 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮(2026-10-17)。
441
+ *
442
+ * 声明与实现的唯一性由 `src/runner/runner-structure.test.ts` 的矩阵用例保证:
443
+ * 每个 kind 的声明值必须等于对应 runner 类是否实现 `runCompact`。bridge 侧
444
+ * 仍按「声明 && 实现」双重门控(fail closed),不一致时打 error 日志。
445
+ */
446
+ export declare const COMPACT_CAPABILITY: Readonly<Record<AgentKind, boolean>>;
447
+ /** 该 agent 是否声明支持 compact(渲染门控与执行门控共用的唯一判据)。 */
448
+ export declare function supportsCompact(kind: AgentKind): boolean;
415
449
  /** Unified session descriptor returned by every AgentSessionReader. */
416
450
  export interface AgentSession {
417
451
  sessionId: string;
@@ -16,4 +16,28 @@
16
16
  * 新增 agent 只改这里。
17
17
  */
18
18
  export const AGENT_KINDS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
19
+ /**
20
+ * Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
21
+ *
22
+ * 键集就是 `AgentKind`:新增 agent 时缺一项 = TypeScript 报错,必须显式回答
23
+ * 「这个 agent 能不能压缩」。历史教训:能力只由 runner 上有没有 `runCompact`
24
+ * 这个鸭子方法隐式决定,dsh 接入时漏了实现,run 卡片直接不渲染按钮——没有
25
+ * 报错、没有置灰、没有日志,用户只能看到一个缺失的按钮(2026-10-17)。
26
+ *
27
+ * 声明与实现的唯一性由 `src/runner/runner-structure.test.ts` 的矩阵用例保证:
28
+ * 每个 kind 的声明值必须等于对应 runner 类是否实现 `runCompact`。bridge 侧
29
+ * 仍按「声明 && 实现」双重门控(fail closed),不一致时打 error 日志。
30
+ */
31
+ export const COMPACT_CAPABILITY = {
32
+ claude: true,
33
+ codex: true,
34
+ opencode: true,
35
+ pi: true,
36
+ kimi: true,
37
+ dsh: true,
38
+ };
39
+ /** 该 agent 是否声明支持 compact(渲染门控与执行门控共用的唯一判据)。 */
40
+ export function supportsCompact(kind) {
41
+ return COMPACT_CAPABILITY[kind];
42
+ }
19
43
  //# sourceMappingURL=types.js.map
@@ -3,31 +3,62 @@
3
3
  *
4
4
  * The AgentSessionReader contract is fully synchronous (file-style readers),
5
5
  * but DSH exposes only async HTTP. The reader therefore bridges with
6
- * `spawnSync('curl', ...)` for its unary calls (session.list / session.history)
6
+ * `spawnSync('curl', ...)` for its unary calls (session/list / session/page)
7
7
  * — a synchronous HTTP round-trip to the local DSH host. This keeps the
8
8
  * router/bridge interface contract intact without touching any upstream logic.
9
9
  *
10
- * listSessions: session.list → filter by cwd → sort by updatedAt desc → local
10
+ * listSessions: session/list → filter by cwd → sort by updatedAt desc → local
11
11
  * pagination (DSH v1 has no cursor pagination).
12
- * readSessionContent: session.history → map SessionEvents to display events +
13
- * session-wide usage accumulation (assistant/message.usage).
12
+ * readSessionContent: session/projections → session/page → map SessionEvents +
13
+ * session-wide usage accumulation (assistant/message.usage);本 run 的 flow
14
+ * 字段 = 末个 turn(turn/start–turn/end)内所有采样之和,与 codex 同口径。
14
15
  */
15
16
  import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
16
17
  interface DshSessionReaderOptions {
17
18
  host?: string;
19
+ /** Configured DSH home; selects `.credentials.yaml` when credentialsFile is unset. */
20
+ dshHomeProvider?: () => string | undefined;
21
+ /** `dsh web` launch token (or its startup URL). */
22
+ token?: string;
23
+ /** DSH credentials file with the durable browser-session signing secret. */
24
+ credentialsFile?: string;
25
+ /** Resolve the credentials file on every call (hot /config change). */
26
+ credentialsFileProvider?: () => string | undefined;
27
+ /** Injectable synchronous credentials reader (tests). */
28
+ readCredentials?: (file: string) => Buffer;
29
+ /** Injectable clock (tests). */
30
+ now?: () => number;
31
+ /** Lifetime of minted browser cookies. */
32
+ cookieLifetimeMs?: number;
33
+ /** Resolve the current token on every call (hot /config change). */
34
+ tokenProvider?: () => string | undefined;
18
35
  /** 每次调用时解析当前 host(/config 修改 host 后 runner 重建、reader 也跟随新 host)。
19
36
  * 优先于固定的 `host`。 */
20
37
  hostProvider?: () => string | undefined;
21
38
  /** Injectable synchronous unary transport. Defaults to spawnSync curl. */
22
- syncRequest?: (baseUrl: string, method: string, payload: unknown) => unknown;
39
+ syncRequest?: (baseUrl: string, method: string, payload: unknown, cookie?: string) => unknown;
40
+ /** Injectable synchronous launch-token bootstrap (tests). */
41
+ syncConnect?: (baseUrl: string, token: string) => string;
23
42
  }
24
43
  export declare class DshSessionReader implements AgentSessionReader {
25
44
  private readonly hostProvider;
45
+ private readonly dshHomeProvider;
46
+ private readonly tokenProvider;
47
+ private readonly credentialsFileProvider;
48
+ private readonly readCredentials;
49
+ private readonly now;
50
+ private readonly cookieLifetimeMs;
26
51
  private readonly syncRequest;
52
+ private readonly syncConnect;
53
+ private readonly cookieCache;
27
54
  constructor(opts?: DshSessionReaderOptions);
28
55
  /** 解析当前 host(hostProvider 优先,允许 /config 热改 host)。 */
29
56
  private resolveBaseUrl;
57
+ private resolveCredentialsFile;
30
58
  private unary;
59
+ /** Mint from the durable secret, or bootstrap and cache a token cookie. */
60
+ private resolveCookie;
61
+ private cookieFromCredentials;
31
62
  listSessions(cwd: string, opts?: {
32
63
  limit?: number;
33
64
  offset?: number;
@@ -36,9 +67,17 @@ export declare class DshSessionReader implements AgentSessionReader {
36
67
  total: number;
37
68
  };
38
69
  getNewestSession(cwd: string): AgentSession | null;
39
- readSessionContent(sessionId: string, _cwd: string, opts?: {
70
+ readSessionContent(sessionId: string, cwd: string, opts?: {
40
71
  maxEvents?: number;
41
72
  }): SessionContent;
42
73
  isSessionActive(sessionId: string, cwd: string): boolean;
74
+ /**
75
+ * Find the session row in `session/list` that matches BOTH id and cwd.
76
+ * Returns undefined when the id belongs to another workspace (or is unknown) —
77
+ * every caller treats that as "not usable here".
78
+ */
79
+ private resolveSession;
80
+ /** `session/page` needs an inclusive cursor; projections carry the watermark. */
81
+ private resolveThroughSeq;
43
82
  }
44
83
  export {};