@easynet/agent-runtime 1.0.3 → 1.0.4

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 (205) hide show
  1. package/.github/workflows/ci.yml +9 -24
  2. package/.github/workflows/release.yml +14 -35
  3. package/agent-runtime/.github/workflows/ci.yml +69 -0
  4. package/agent-runtime/.github/workflows/release.yml +118 -0
  5. package/agent-runtime/.releaserc.cjs +26 -0
  6. package/agent-runtime/config/agent.deep.yaml +25 -0
  7. package/agent-runtime/config/agent.react.yaml +24 -0
  8. package/agent-runtime/example/basic-usage.ts +49 -0
  9. package/agent-runtime/package-lock.json +7740 -0
  10. package/agent-runtime/package.json +49 -0
  11. package/agent-runtime/pnpm-lock.yaml +3712 -0
  12. package/agent-runtime/scripts/resolve-deps.js +54 -0
  13. package/agent-runtime/src/agents/deep-agent.ts +165 -0
  14. package/agent-runtime/src/agents/react-agent.helpers.ts +227 -0
  15. package/agent-runtime/src/agents/react-agent.ts +584 -0
  16. package/{src → agent-runtime/src/agents}/sub-agent.ts +2 -2
  17. package/agent-runtime/src/cli/args.ts +15 -0
  18. package/agent-runtime/src/cli/event-listener.ts +162 -0
  19. package/agent-runtime/src/cli/interactive.ts +144 -0
  20. package/agent-runtime/src/cli/runtime.ts +31 -0
  21. package/agent-runtime/src/cli/spinner.ts +23 -0
  22. package/agent-runtime/src/cli/terminal-render.ts +322 -0
  23. package/agent-runtime/src/cli/types.ts +33 -0
  24. package/agent-runtime/src/cli.ts +134 -0
  25. package/agent-runtime/src/config/helpers.ts +179 -0
  26. package/agent-runtime/src/config/index.ts +245 -0
  27. package/agent-runtime/src/config/types.ts +62 -0
  28. package/agent-runtime/src/core/context.ts +266 -0
  29. package/agent-runtime/src/index.ts +55 -0
  30. package/agent-runtime/tsconfig.json +18 -0
  31. package/apps/imessagebot/README.md +38 -0
  32. package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/README.md +33 -0
  33. package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/package-lock.json +15257 -0
  34. package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/package.json +55 -0
  35. package/apps/imessagebot/config/agents/deep/agent.yaml +31 -0
  36. package/apps/imessagebot/config/agents/react/agent.yaml +58 -0
  37. package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/README.md +33 -0
  38. package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/package-lock.json +15457 -0
  39. package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/package.json +55 -0
  40. package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/README.md +33 -0
  41. package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/package-lock.json +15257 -0
  42. package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/package.json +62 -0
  43. package/apps/imessagebot/config/agents/shared/memory.yaml +31 -0
  44. package/apps/imessagebot/config/agents/shared/model.yaml +23 -0
  45. package/apps/imessagebot/config/agents/shared/tool.yaml +13 -0
  46. package/apps/imessagebot/config/app.yaml +14 -0
  47. package/apps/imessagebot/package-lock.json +53695 -0
  48. package/apps/imessagebot/package.json +41 -0
  49. package/apps/imessagebot/pnpm-lock.yaml +1589 -0
  50. package/apps/imessagebot/scripts/resolve-deps.js +41 -0
  51. package/apps/imessagebot/scripts/test-llm.mjs +27 -0
  52. package/apps/imessagebot/scripts/validate-tools-config.mjs +174 -0
  53. package/apps/imessagebot/src/config.ts +76 -0
  54. package/apps/imessagebot/src/context.ts +35 -0
  55. package/apps/imessagebot/src/index.ts +17 -0
  56. package/apps/imessagebot/tsconfig.json +18 -0
  57. package/apps/itermbot/.github/workflows/ci.yml +61 -0
  58. package/apps/itermbot/.github/workflows/release.yml +80 -0
  59. package/apps/itermbot/.releaserc.cjs +26 -0
  60. package/apps/itermbot/README.md +82 -0
  61. package/apps/itermbot/config/app.yaml +29 -0
  62. package/apps/itermbot/config/tsconfig.json +18 -0
  63. package/apps/itermbot/macos_disk_usage_agent_plan.md +244 -0
  64. package/apps/itermbot/package-lock.json +53697 -0
  65. package/apps/itermbot/package.json +57 -0
  66. package/apps/itermbot/pnpm-lock.yaml +3966 -0
  67. package/apps/itermbot/scripts/patch-buildin-cache.sh +25 -0
  68. package/apps/itermbot/scripts/resolve-deps.js +41 -0
  69. package/apps/itermbot/scripts/test-llm.mjs +32 -0
  70. package/apps/itermbot/skills/command-explain-and-guard/SKILL.md +39 -0
  71. package/apps/itermbot/skills/command-explain-and-guard/handler.js +86 -0
  72. package/apps/itermbot/skills/disk-usage-investigate/SKILL.md +44 -0
  73. package/apps/itermbot/skills/disk-usage-investigate/handler.js +12 -0
  74. package/apps/itermbot/skills/gpu-ssh-monitor/SKILL.md +64 -0
  75. package/apps/itermbot/skills/repo-triage/SKILL.md +40 -0
  76. package/apps/itermbot/skills/repo-triage/handler.js +56 -0
  77. package/apps/itermbot/skills/test-failure-diagnose/SKILL.md +43 -0
  78. package/apps/itermbot/skills/test-failure-diagnose/handler.js +107 -0
  79. package/apps/itermbot/src/config.ts +95 -0
  80. package/apps/itermbot/src/context.ts +35 -0
  81. package/apps/itermbot/src/index.ts +223 -0
  82. package/apps/itermbot/src/iterm/session-hint.ts +40 -0
  83. package/apps/itermbot/src/iterm/target-routing.ts +419 -0
  84. package/apps/itermbot/src/startup/colors.ts +317 -0
  85. package/apps/itermbot/src/startup/diagnostics.ts +97 -0
  86. package/apps/itermbot/src/startup/ui.ts +141 -0
  87. package/config/agent.deep.yaml +25 -0
  88. package/config/agent.react.yaml +24 -0
  89. package/dist/agents/deep-agent.d.ts +37 -0
  90. package/dist/agents/deep-agent.d.ts.map +1 -0
  91. package/dist/agents/deep-agent.js +115 -0
  92. package/dist/agents/deep-agent.js.map +1 -0
  93. package/dist/agents/react-agent.d.ts +40 -0
  94. package/dist/agents/react-agent.d.ts.map +1 -0
  95. package/dist/agents/react-agent.helpers.d.ts +40 -0
  96. package/dist/agents/react-agent.helpers.d.ts.map +1 -0
  97. package/dist/agents/react-agent.helpers.js +196 -0
  98. package/dist/agents/react-agent.helpers.js.map +1 -0
  99. package/dist/agents/react-agent.js +400 -0
  100. package/dist/agents/react-agent.js.map +1 -0
  101. package/dist/agents/sub-agent.d.ts +34 -0
  102. package/dist/agents/sub-agent.d.ts.map +1 -0
  103. package/dist/agents/sub-agent.js +53 -0
  104. package/dist/agents/sub-agent.js.map +1 -0
  105. package/dist/cli/args.d.ts +8 -0
  106. package/dist/cli/args.d.ts.map +1 -0
  107. package/dist/cli/args.js +9 -0
  108. package/dist/cli/args.js.map +1 -0
  109. package/dist/cli/event-listener.d.ts +3 -0
  110. package/dist/cli/event-listener.d.ts.map +1 -0
  111. package/dist/cli/event-listener.js +131 -0
  112. package/dist/cli/event-listener.js.map +1 -0
  113. package/dist/cli/interactive.d.ts +4 -0
  114. package/dist/cli/interactive.d.ts.map +1 -0
  115. package/dist/cli/interactive.js +118 -0
  116. package/dist/cli/interactive.js.map +1 -0
  117. package/dist/cli/runtime.d.ts +8 -0
  118. package/dist/cli/runtime.d.ts.map +1 -0
  119. package/dist/cli/runtime.js +27 -0
  120. package/dist/cli/runtime.js.map +1 -0
  121. package/dist/cli/spinner.d.ts +2 -0
  122. package/dist/cli/spinner.d.ts.map +1 -0
  123. package/dist/cli/spinner.js +22 -0
  124. package/dist/cli/spinner.js.map +1 -0
  125. package/dist/cli/terminal-render.d.ts +7 -0
  126. package/dist/cli/terminal-render.d.ts.map +1 -0
  127. package/dist/cli/terminal-render.js +282 -0
  128. package/dist/cli/terminal-render.js.map +1 -0
  129. package/dist/cli/types.d.ts +29 -0
  130. package/dist/cli/types.d.ts.map +1 -0
  131. package/dist/cli/types.js +3 -0
  132. package/dist/cli/types.js.map +1 -0
  133. package/dist/cli.d.ts +4 -41
  134. package/dist/cli.d.ts.map +1 -1
  135. package/dist/cli.js +84 -588
  136. package/dist/cli.js.map +1 -1
  137. package/dist/config/helpers.d.ts +6 -0
  138. package/dist/config/helpers.d.ts.map +1 -0
  139. package/dist/config/helpers.js +164 -0
  140. package/dist/config/helpers.js.map +1 -0
  141. package/dist/config/index.d.ts +15 -0
  142. package/dist/config/index.d.ts.map +1 -0
  143. package/dist/config/index.js +160 -0
  144. package/dist/config/index.js.map +1 -0
  145. package/dist/config/types.d.ts +57 -0
  146. package/dist/config/types.d.ts.map +1 -0
  147. package/dist/config/types.js +2 -0
  148. package/dist/config/types.js.map +1 -0
  149. package/dist/context.d.ts +8 -69
  150. package/dist/context.d.ts.map +1 -1
  151. package/dist/context.js +44 -24
  152. package/dist/context.js.map +1 -1
  153. package/dist/core/context.d.ts +66 -0
  154. package/dist/core/context.d.ts.map +1 -0
  155. package/dist/core/context.js +149 -0
  156. package/dist/core/context.js.map +1 -0
  157. package/dist/deep-agent.d.ts +5 -2
  158. package/dist/deep-agent.d.ts.map +1 -1
  159. package/dist/deep-agent.js +44 -11
  160. package/dist/deep-agent.js.map +1 -1
  161. package/dist/index.d.ts +6 -6
  162. package/dist/index.d.ts.map +1 -1
  163. package/dist/index.js +6 -6
  164. package/dist/index.js.map +1 -1
  165. package/dist/middleware/malformed-tool-call-middleware.d.ts +8 -0
  166. package/dist/middleware/malformed-tool-call-middleware.d.ts.map +1 -0
  167. package/dist/middleware/malformed-tool-call-middleware.js +191 -0
  168. package/dist/middleware/malformed-tool-call-middleware.js.map +1 -0
  169. package/dist/react-agent.d.ts +2 -2
  170. package/dist/react-agent.d.ts.map +1 -1
  171. package/dist/react-agent.js +28 -9
  172. package/dist/react-agent.js.map +1 -1
  173. package/package.json +1 -1
  174. package/scripts/resolve-deps.js +54 -0
  175. package/src/agents/deep-agent.ts +165 -0
  176. package/src/agents/react-agent.helpers.ts +227 -0
  177. package/src/agents/react-agent.ts +584 -0
  178. package/src/agents/sub-agent.ts +82 -0
  179. package/src/cli/args.ts +15 -0
  180. package/src/cli/event-listener.ts +162 -0
  181. package/src/cli/interactive.ts +144 -0
  182. package/src/cli/runtime.ts +31 -0
  183. package/src/cli/spinner.ts +23 -0
  184. package/src/cli/terminal-render.ts +322 -0
  185. package/src/cli/types.ts +33 -0
  186. package/src/cli.ts +91 -702
  187. package/src/config/helpers.ts +179 -0
  188. package/src/config/index.ts +245 -0
  189. package/src/config/types.ts +62 -0
  190. package/src/core/context.ts +266 -0
  191. package/src/index.ts +13 -11
  192. package/src/middleware/malformed-tool-call-middleware.ts +239 -0
  193. package/src/types/markdown-it-terminal.d.ts +4 -0
  194. package/src/types/marked-terminal.d.ts +16 -0
  195. package/dist/config.d.ts +0 -86
  196. package/dist/config.d.ts.map +0 -1
  197. package/dist/config.js +0 -84
  198. package/dist/config.js.map +0 -1
  199. package/src/config.ts +0 -177
  200. package/src/context.ts +0 -247
  201. package/src/deep-agent.ts +0 -104
  202. package/src/react-agent.ts +0 -576
  203. /package/{src → agent-runtime/src/middleware}/malformed-tool-call-middleware.ts +0 -0
  204. /package/{src → agent-runtime/src/types}/markdown-it-terminal.d.ts +0 -0
  205. /package/{src → agent-runtime/src/types}/marked-terminal.d.ts +0 -0
package/src/cli.ts CHANGED
@@ -2,741 +2,130 @@
2
2
  * Shared CLI runner for agent apps.
3
3
  * Provides: arg parsing, single-shot mode, interactive REPL, event logging.
4
4
  */
5
- import os from "node:os";
6
- import { createInterface } from "node:readline";
7
- import MarkdownIt from "markdown-it";
8
- import markdownItTerminal from "markdown-it-terminal";
9
- import {
10
- createConsoleAgentEventListener,
11
- type AgentEvent,
12
- type AgentEventListener,
13
- } from "@easynet/agent-common";
14
- import { createReactAgent, type ReactAgentRuntime } from "./react-agent.js";
15
- import { createDeepAgent, type DeepAgentRuntime } from "./deep-agent.js";
16
- import { malformedToolCallMiddleware } from "./malformed-tool-call-middleware.js";
17
- import type { BotContext } from "./context.js";
18
-
19
- const REACT = "react" as const;
20
- const DEEP = "deep" as const;
21
-
22
- type AgentKind = typeof REACT | typeof DEEP;
23
-
24
- export interface AppCliUiOptions {
25
- /** Prompt/section label for user. Default: current OS user (username(uid)). */
26
- userLabel?: string;
27
- /** Prompt/section label for assistant. Default: agent kind (ReAct/Deep). */
28
- assistantLabel?: string;
29
- /** Enable ANSI colors in interactive output. Default: true when TTY and NO_COLOR is not set. */
30
- useColor?: boolean;
31
- /** Render assistant output as terminal-friendly markdown. Default: true. */
32
- renderMarkdown?: boolean;
33
- /** Echo the user question in the response section. Default: true. */
34
- echoUserQuestion?: boolean;
35
- /** Startup loading line. Set false to suppress. */
36
- loadingText?: string | false;
37
- /** Show animated loading progress while creating bot context. */
38
- loadingSpinner?: boolean;
39
- /** Startup ready line. Set false to suppress. */
40
- readyText?: string | false;
41
- /** Interactive intro line before prompt. Set false to suppress. */
42
- interactiveIntro?: string | false;
43
- /** Show animated spinner while a user request is being processed. */
44
- processingSpinner?: boolean;
45
- /** Spinner label shown while processing. Set false to suppress label/spinner. */
46
- processingText?: string | false;
47
- }
48
-
49
- export interface AppCliOptions {
50
- appName: string;
51
- createBotContext: () => Promise<BotContext>;
52
- /** Extra interactive commands, e.g. { "list tools": (ctx) => ... } */
53
- interactiveCommands?: Record<string, (ctx: BotContext) => void | Promise<void>>;
54
- /** Extra event listener(s) for structured runtime logs. */
55
- eventListener?: AgentEventListener | AgentEventListener[];
56
- /** Called after context is ready, before the REPL starts. Failures are logged but do not crash. */
57
- onReady?: (ctx: BotContext) => void | Promise<void>;
58
- /** Called during shutdown (exit/signal). Should be synchronous and fast. */
59
- onShutdown?: (ctx: BotContext) => void;
60
- /** Interactive UI style options. */
61
- ui?: AppCliUiOptions;
62
- }
63
-
64
- function parseArgs(): { kind: AgentKind; query?: string } {
65
- const args = process.argv.slice(2);
66
- const first = args[0]?.toLowerCase();
67
- const kind: AgentKind = first === DEEP ? DEEP : REACT;
68
- const query =
69
- first === REACT || first === DEEP ? (args[1] ?? undefined) : (first ?? undefined);
70
- return { kind, query };
71
- }
72
-
73
- function escapeRegExp(value: string): string {
74
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
- }
76
-
77
- async function createRuntime(
78
- ctx: BotContext,
79
- kind: AgentKind
80
- ): Promise<ReactAgentRuntime | DeepAgentRuntime> {
81
- if (kind === REACT) {
82
- return createReactAgent(ctx, {
83
- middleware: [malformedToolCallMiddleware()],
84
- });
85
- }
86
- return createDeepAgent(ctx);
87
- }
88
-
89
- async function runOne(ctx: BotContext, kind: AgentKind, query: string): Promise<string> {
90
- const runtime = await createRuntime(ctx, kind);
91
- const { text } = await runtime.run(query);
92
- return text;
93
- }
94
-
95
- function createAnsi(useColor: boolean): Record<string, string> {
96
- return {
97
- reset: useColor ? "\x1b[0m" : "",
98
- dim: useColor ? "\x1b[2m" : "",
99
- heading: useColor ? "\x1b[1;38;5;45m" : "",
100
- bullet: useColor ? "\x1b[38;5;81m" : "",
101
- code: useColor ? "\x1b[38;5;221m" : "",
102
- quote: useColor ? "\x1b[38;5;245m" : "",
103
- hr: useColor ? "\x1b[38;5;240m" : "",
5
+ import { createConsoleAgentEventListener } from "@easynet/agent-common";
6
+ import { parseArgs } from "./cli/args.js";
7
+ import { createStructuredRunEventListener } from "./cli/event-listener.js";
8
+ import { interactive } from "./cli/interactive.js";
9
+ import { readDefaultAgentKindFromConfig, runOne } from "./cli/runtime.js";
10
+ import { startLoadingSpinner } from "./cli/spinner.js";
11
+ import { renderForTerminal } from "./cli/terminal-render.js";
12
+ import { REACT, type AgentKind, type AppCliOptions, type AppCliUiOptions } from "./cli/types.js";
13
+
14
+ export type { AppCliOptions, AppCliUiOptions };
15
+ export { createStructuredRunEventListener };
16
+
17
+ function createExitHandler(
18
+ appName: string,
19
+ onShutdown: AppCliOptions["onShutdown"],
20
+ ctx: Awaited<ReturnType<AppCliOptions["createBotContext"]>>,
21
+ ): (code: number) => never {
22
+ let didCleanup = false;
23
+
24
+ const cleanup = () => {
25
+ if (didCleanup) return;
26
+ didCleanup = true;
27
+ try {
28
+ onShutdown?.(ctx);
29
+ } catch (err) {
30
+ console.error(`${appName}: onShutdown hook failed:`, err instanceof Error ? err.message : err);
31
+ }
104
32
  };
105
- }
106
-
107
- function styleInlineMarkdown(line: string, ansi: Record<string, string>): string {
108
- return line
109
- .replace(/`([^`]+)`/g, `${ansi.code}\`$1\`${ansi.reset}`)
110
- .replace(/\*\*([^*]+)\*\*/g, `${ansi.heading}$1${ansi.reset}`);
111
- }
112
-
113
- function parseTableRow(line: string): string[] {
114
- const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
115
- return trimmed.split("|").map((cell) => cell.trim());
116
- }
117
33
 
118
- function isTableSeparatorRow(cells: string[]): boolean {
119
- return cells.length > 0 && cells.every((c) => /^:?-{3,}:?$/.test(c));
34
+ process.once("exit", cleanup);
35
+ const exitApp = (code: number): never => {
36
+ cleanup();
37
+ process.exit(code);
38
+ };
39
+ process.once("SIGINT", () => exitApp(130));
40
+ process.once("SIGTERM", () => exitApp(143));
41
+ return exitApp;
120
42
  }
121
43
 
122
- function renderTable(rows: string[][], ansi: Record<string, string>): string[] {
123
- const normalized = rows.map((r) => [...r]);
124
- const colCount = Math.max(...normalized.map((r) => r.length));
125
- for (const row of normalized) {
126
- while (row.length < colCount) row.push("");
44
+ function subscribeEventListeners(options: AppCliOptions, ctx: Awaited<ReturnType<AppCliOptions["createBotContext"]>>): void {
45
+ if (process.env.AGENT_EVENT_STDERR === "1") {
46
+ ctx.events.subscribe(createConsoleAgentEventListener());
127
47
  }
128
48
 
129
- const widths = Array.from({ length: colCount }, (_, i) =>
130
- Math.max(...normalized.map((r) => (r[i] ?? "").length)),
131
- );
132
-
133
- const line = `+${widths.map((w) => "-".repeat(w + 2)).join("+")}+`;
134
- const out: string[] = [`${ansi.hr}${line}${ansi.reset}`];
135
-
136
- normalized.forEach((row, idx) => {
137
- const body = row
138
- .map((cell, i) => ` ${(cell ?? "").padEnd(widths[i] ?? 0)} `)
139
- .join("|");
140
- const styledBody =
141
- idx === 0
142
- ? `${ansi.heading}|${styleInlineMarkdown(body, ansi)}|${ansi.reset}`
143
- : `|${styleInlineMarkdown(body, ansi)}|`;
144
- out.push(styledBody);
145
- if (idx === 0) out.push(`${ansi.hr}${line}${ansi.reset}`);
146
- });
147
-
148
- out.push(`${ansi.hr}${line}${ansi.reset}`);
149
- return out;
150
- }
151
-
152
- function formatMarkdownForTerminal(
153
- markdown: string,
154
- options: { useColor: boolean } = { useColor: true },
155
- ): string {
156
- const ansi = createAnsi(options.useColor);
157
- const lines = markdown.replace(/\r\n/g, "\n").split("\n");
158
- const out: string[] = [];
159
- let inCodeBlock = false;
160
- let codeFenceLang = "";
161
- let codeBoxWidth = 0;
162
-
163
- for (let i = 0; i < lines.length; i += 1) {
164
- const rawLine = lines[i];
165
- const line = rawLine ?? "";
166
-
167
- if (line.trim().startsWith("```")) {
168
- if (!inCodeBlock) {
169
- inCodeBlock = true;
170
- codeFenceLang = line.trim().slice(3).trim();
171
- const title = codeFenceLang ? ` code:${codeFenceLang} ` : " code ";
172
- codeBoxWidth = Math.max(16, title.length + 4);
173
- out.push(`${ansi.hr}┌${"─".repeat(codeBoxWidth)}┐${ansi.reset}`);
174
- out.push(`${ansi.hr}│${ansi.reset}${ansi.dim}${title.padEnd(codeBoxWidth)}${ansi.reset}${ansi.hr}│${ansi.reset}`);
175
- } else {
176
- inCodeBlock = false;
177
- codeFenceLang = "";
178
- out.push(`${ansi.hr}└${"─".repeat(codeBoxWidth)}┘${ansi.reset}`);
179
- }
180
- continue;
181
- }
182
-
183
- if (inCodeBlock) {
184
- const content = line.length > codeBoxWidth ? line.slice(0, codeBoxWidth) : line.padEnd(codeBoxWidth);
185
- out.push(`${ansi.hr}│${ansi.reset}${ansi.code}${content}${ansi.reset}${ansi.hr}│${ansi.reset}`);
186
- continue;
187
- }
188
-
189
- // Markdown table: header row + separator row + data rows
190
- if (
191
- line.includes("|") &&
192
- i + 1 < lines.length &&
193
- (lines[i + 1] ?? "").includes("|")
194
- ) {
195
- const header = parseTableRow(line);
196
- const sep = parseTableRow(lines[i + 1] ?? "");
197
- if (header.length > 1 && header.length === sep.length && isTableSeparatorRow(sep)) {
198
- const rows: string[][] = [header];
199
- let j = i + 2;
200
- while (j < lines.length && (lines[j] ?? "").includes("|")) {
201
- rows.push(parseTableRow(lines[j] ?? ""));
202
- j += 1;
203
- }
204
- out.push(...renderTable(rows, ansi));
205
- i = j - 1;
206
- continue;
207
- }
208
- }
209
-
210
- if (/^\s*#{1,6}\s+/.test(line)) {
211
- const headingText = line.replace(/^\s*#{1,6}\s+/, "").trim();
212
- out.push(`${ansi.heading}${headingText}${ansi.reset}`);
213
- continue;
214
- }
215
-
216
- if (/^\s*>\s?/.test(line)) {
217
- out.push(`${ansi.quote}${line}${ansi.reset}`);
218
- continue;
219
- }
220
-
221
- if (/^\s*([-*]|\d+\.)\s+/.test(line)) {
222
- out.push(`${ansi.bullet}${styleInlineMarkdown(line, ansi)}${ansi.reset}`);
223
- continue;
224
- }
225
-
226
- if (/^\s*---+\s*$/.test(line)) {
227
- out.push(`${ansi.hr}${"─".repeat(56)}${ansi.reset}`);
228
- continue;
229
- }
230
-
231
- out.push(styleInlineMarkdown(line, ansi));
49
+ const listeners = Array.isArray(options.eventListener)
50
+ ? options.eventListener
51
+ : options.eventListener
52
+ ? [options.eventListener]
53
+ : [];
54
+ for (const listener of listeners) {
55
+ ctx.events.subscribe(listener);
232
56
  }
233
-
234
- return out.join("\n");
235
- }
236
-
237
- const markdownRenderers = new Map<"color" | "plain", MarkdownIt>();
238
-
239
- function getMarkdownRenderer(useColor: boolean): MarkdownIt {
240
- const key = useColor ? "color" : "plain";
241
- const cached = markdownRenderers.get(key);
242
- if (cached) return cached;
243
-
244
- const md = new MarkdownIt({
245
- html: false,
246
- linkify: true,
247
- typographer: true,
248
- breaks: false,
249
- });
250
- md.use(markdownItTerminal, {
251
- indent: "",
252
- ...(useColor
253
- ? {}
254
- : {
255
- styleOptions: {
256
- code: (s: string) => s,
257
- blockquote: (s: string) => s,
258
- html: (s: string) => s,
259
- heading: (s: string) => s,
260
- firstHeading: (s: string) => s,
261
- hr: (s: string) => s,
262
- listitem: (s: string) => s,
263
- table: (s: string) => s,
264
- paragraph: (s: string) => s,
265
- strong: (s: string) => s,
266
- em: (s: string) => s,
267
- codespan: (s: string) => s,
268
- del: (s: string) => s,
269
- link: (s: string) => s,
270
- href: (s: string) => s,
271
- },
272
- }),
273
- });
274
-
275
- markdownRenderers.set(key, md);
276
- return md;
277
57
  }
278
58
 
279
- function normalizeAssistantMarkdown(text: string): string {
280
- const source = text.replace(/\r\n/g, "\n");
281
- const hasMarkdownHeadings = /^\s*#{1,6}\s+/m.test(source);
282
- const sanitizeLine = (line: string): string => {
283
- let next = line;
284
- const trimmed = next.trim();
285
- // Remove stray inline fence markers that frequently break markdown rendering.
286
- if (next.includes("```") && !trimmed.startsWith("```")) {
287
- next = next.replace(/```+/g, "").replace(/\s+$/, "");
288
- }
289
- // Keep headings stable and readable in terminal output.
290
- if (/^\s*#{3,}\s+/.test(next)) {
291
- next = next.replace(/^\s*#{3,}\s+/, "## ");
292
- }
293
- return next;
294
- };
295
- const normalizeListLine = (line: string): string => {
296
- if (/^(\s{2,}|\t+)([*-])\s+/.test(line)) {
297
- return line.replace(/^(\s{2,}|\t+)([*-])\s+/, "- ");
298
- }
299
- if (/^(\s{2,}|\t+)(\d+\.)\s+/.test(line)) {
300
- return line.replace(/^(\s{2,}|\t+)(\d+\.)\s+/, "$2 ");
301
- }
302
- return line;
303
- };
304
- const lines = source
305
- .split("\n")
306
- .map(sanitizeLine)
307
- .map(normalizeListLine);
308
-
309
- const titled = lines.map((line) => {
310
- const trimmed = line.trim();
311
- const bulletHeading = trimmed.match(/^[-*]\s+\*\*([^*]+)\*\*:?\s*$/);
312
- if (bulletHeading?.[1]) {
313
- return `### ${bulletHeading[1].trim()}`;
314
- }
315
- const plainBulletHeading = trimmed.match(/^[-*]\s+([^`].+):\s*$/);
316
- if (plainBulletHeading?.[1] && plainBulletHeading[1].length <= 48) {
317
- return `### ${plainBulletHeading[1].trim()}`;
318
- }
319
- if (hasMarkdownHeadings) return trimmed === "Summary (3‑8 bullets)" ? "## Summary" : line;
320
- const unwrapped = trimmed.replace(/^\*\*(.+)\*\*$/, "$1").trim();
321
- if (/^(Key terminal output.*|Current terminal buffer.*|Summary.*|Next steps.*)$/i.test(unwrapped)) {
322
- return `## ${unwrapped}`;
323
- }
324
- if (/^(Key observations|Findings|Analysis|Conclusion)$/i.test(unwrapped)) {
325
- return `## ${unwrapped}`;
326
- }
327
- return line;
328
- });
329
-
330
- // Merge accidental line breaks inside list items so markdown renders as one bullet.
331
- const merged: string[] = [];
332
- for (let i = 0; i < titled.length; i += 1) {
333
- const current = titled[i] ?? "";
334
- const next = titled[i + 1] ?? "";
335
- const currentTrim = current.trim();
336
- const nextTrim = next.trim();
337
- const isListLine = /^([-*]|\d+\.)\s+/.test(currentTrim);
338
- const nextStartsNewBlock =
339
- nextTrim.length === 0 ||
340
- /^([-*]|\d+\.)\s+/.test(nextTrim) ||
341
- /^#{1,6}\s+/.test(nextTrim) ||
342
- /^```/.test(nextTrim) ||
343
- /^---+$/.test(nextTrim);
344
- const looksLikeWrappedContinuation =
345
- isListLine &&
346
- !nextStartsNewBlock &&
347
- /^[0-9./~]/.test(nextTrim);
348
-
349
- if (looksLikeWrappedContinuation) {
350
- merged.push(`${current.replace(/\s+$/, "")} ${nextTrim}`);
351
- i += 1;
352
- continue;
353
- }
354
- merged.push(current);
59
+ async function runOnReady(options: AppCliOptions, appName: string, ctx: Awaited<ReturnType<AppCliOptions["createBotContext"]>>): Promise<void> {
60
+ if (!options.onReady) return;
61
+ try {
62
+ await options.onReady(ctx);
63
+ } catch (err) {
64
+ console.error(`${appName}: onReady hook failed:`, err instanceof Error ? err.message : err);
355
65
  }
66
+ }
356
67
 
357
- const out: string[] = [];
358
- for (let i = 0; i < merged.length; i += 1) {
359
- const line = merged[i] ?? "";
360
- out.push(line);
361
-
362
- const heading = line.trim();
363
- const isOutputHeading = /^##\s*(Key terminal output|Current terminal buffer|Terminal output)/i.test(heading);
364
- if (!isOutputHeading) continue;
365
- if ((merged[i + 1] ?? "").trim().startsWith("```")) continue;
366
-
367
- let j = i + 1;
368
- const block: string[] = [];
369
- while (j < merged.length) {
370
- const current = merged[j] ?? "";
371
- if (!current.trim()) {
372
- if (block.length === 0) {
373
- j += 1;
374
- continue;
375
- }
376
- break;
377
- }
378
- if (!/^\s{2,}\S/.test(current)) break;
379
- block.push(current.replace(/^\s+/, ""));
380
- j += 1;
381
- }
68
+ async function createContextWithLoading(options: AppCliOptions): Promise<Awaited<ReturnType<AppCliOptions["createBotContext"]>>> {
69
+ const loadingText = options.ui?.loadingText ?? `${options.appName}: loading config, LLM, memory, tools...`;
70
+ const useLoadingSpinner = options.ui?.loadingSpinner ?? false;
71
+ let stopLoadingSpinner: (() => void) | null = null;
382
72
 
383
- if (block.length >= 2) {
384
- out.push("```text");
385
- out.push(...block);
386
- out.push("```");
387
- i = j - 1;
73
+ if (loadingText !== false) {
74
+ if (useLoadingSpinner && Boolean(process.stderr.isTTY)) {
75
+ stopLoadingSpinner = startLoadingSpinner(loadingText);
76
+ } else {
77
+ console.error(loadingText);
388
78
  }
389
79
  }
390
80
 
391
- const fencedCount = out.reduce((count, line) => count + (/^\s*```/.test(line) ? 1 : 0), 0);
392
- if (fencedCount % 2 === 1) {
393
- out.push("```");
394
- }
395
-
396
- return out.join("\n");
81
+ const ctx = await options.createBotContext();
82
+ stopLoadingSpinner?.();
83
+ return ctx;
397
84
  }
398
85
 
399
- function renderForTerminal(
400
- text: string,
401
- options: { renderMarkdown: boolean; useColor: boolean },
402
- ): string {
403
- if (!options.renderMarkdown) return text;
404
- const normalizedText = normalizeAssistantMarkdown(text);
405
- try {
406
- return getMarkdownRenderer(options.useColor).render(normalizedText, {});
407
- } catch {
408
- return formatMarkdownForTerminal(normalizedText, { useColor: options.useColor });
86
+ function printReady(options: AppCliOptions, kind: AgentKind): void {
87
+ const agentLabel = kind === REACT ? "ReAct (LangChain)" : "Deep (DeepAgents)";
88
+ const readyText = options.ui?.readyText ?? `Ready. Agent: ${agentLabel}`;
89
+ if (readyText !== false) {
90
+ console.error(readyText);
409
91
  }
410
92
  }
411
93
 
412
- function startLoadingSpinner(message: string): () => void {
413
- const frames = ["", ".", "..", "..."];
414
- let frameIndex = 0;
415
- let lastLength = 0;
416
- const render = () => {
417
- const frame = frames[frameIndex % frames.length] ?? "";
418
- const trimmed = message.trim();
419
- const base = trimmed.startsWith("⏳") ? trimmed : `⏳ ${trimmed}`;
420
- const line = `${base}${frame}`;
421
- const padded = lastLength > line.length ? line.padEnd(lastLength, " ") : line;
422
- process.stderr.write(`\r${padded}\r`);
423
- lastLength = padded.length;
424
- frameIndex += 1;
425
- };
426
- render();
427
- const timer = setInterval(render, 90);
428
- return () => {
429
- clearInterval(timer);
430
- process.stderr.write(`\r${" ".repeat(lastLength)}\r`);
431
- };
94
+ function resolveAgentKind(parsed: ReturnType<typeof parseArgs>, ctx: Awaited<ReturnType<AppCliOptions["createBotContext"]>>): AgentKind {
95
+ if (parsed.explicitKind) return parsed.kind;
96
+ return readDefaultAgentKindFromConfig(ctx) ?? parsed.kind;
432
97
  }
433
98
 
434
- async function interactive(
435
- ctx: BotContext,
436
- kind: AgentKind,
99
+ async function runSingleQuery(
437
100
  options: AppCliOptions,
438
- exitApp: (code: number) => never
439
- ) {
440
- const name = kind === REACT ? "ReAct" : "Deep";
441
- const runtime = await createRuntime(ctx, kind);
442
- const userInfo = os.userInfo();
443
- const userLabel = options.ui?.userLabel ?? `${userInfo.username}`;
444
- const assistantLabel = options.ui?.assistantLabel ?? name;
101
+ ctx: Awaited<ReturnType<AppCliOptions["createBotContext"]>>,
102
+ kind: AgentKind,
103
+ query: string,
104
+ ): Promise<void> {
105
+ const text = await runOne(ctx, kind, query);
445
106
  const useColor = options.ui?.useColor ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
446
107
  const renderMarkdown = options.ui?.renderMarkdown ?? true;
447
- const echoUserQuestion = options.ui?.echoUserQuestion ?? true;
448
- const showProcessingSpinner =
449
- options.ui?.processingSpinner ?? Boolean(process.stderr.isTTY);
450
- const processingText = options.ui?.processingText ?? "Processing";
451
-
452
- const color = {
453
- reset: useColor ? "\x1b[0m" : "",
454
- dim: useColor ? "\x1b[2m" : "",
455
- user: useColor ? "\x1b[38;5;39m" : "",
456
- bot: useColor ? "\x1b[38;5;48m" : "",
457
- prompt: useColor ? "\x1b[38;5;245m" : "",
458
- promptUser: useColor ? "\x1b[1;38;5;45m" : "",
459
- };
460
- const hr = `${color.dim}${"-".repeat(56)}${color.reset}`;
461
- const promptText = `${color.prompt}[${color.promptUser}${userLabel}${color.reset}${color.prompt}]${color.reset} `;
462
-
463
- const rl = createInterface({ input: process.stdin, output: process.stdout });
464
- const userLabelPrefixPattern = new RegExp(`^(?:\\[${escapeRegExp(userLabel)}\\]\\s*)+`, "i");
465
- const prompt = () =>
466
- rl.question(promptText, async (line) => {
467
- const qRaw = line?.trim();
468
- const q = qRaw?.replace(userLabelPrefixPattern, "").trim();
469
- if (!q) {
470
- prompt();
471
- return;
472
- }
473
- if (q === "exit" || q === "quit") {
474
- rl.close();
475
- exitApp(0);
476
- }
477
-
478
- const handler = options.interactiveCommands?.[q.toLowerCase()];
479
- if (handler) {
480
- await handler(ctx);
481
- prompt();
482
- return;
483
- }
484
-
485
- let stopProcessingSpinner: (() => void) | null = null;
486
- try {
487
- console.log(`\n${hr}`);
488
- if (echoUserQuestion) {
489
- console.log(`${color.user}[${userLabel}]${color.reset}`);
490
- console.log(`> ${q}`);
491
- console.log("");
492
- }
493
- console.log(`${color.bot}[${assistantLabel}]${color.reset}`);
494
- stopProcessingSpinner = showProcessingSpinner
495
- ? startLoadingSpinner(processingText === false ? "⏳" : `⏳ ${processingText}`)
496
- : null;
497
- const { text } = await runtime.run(q);
498
- if (stopProcessingSpinner) {
499
- stopProcessingSpinner();
500
- stopProcessingSpinner = null;
501
- }
502
- console.log(renderForTerminal(text, { renderMarkdown, useColor }));
503
- console.log(`${hr}\n`);
504
- } catch (err) {
505
- if (stopProcessingSpinner) {
506
- stopProcessingSpinner();
507
- stopProcessingSpinner = null;
508
- }
509
- console.error("Error:", err instanceof Error ? err.message : err);
510
- } finally {
511
- if (stopProcessingSpinner) {
512
- stopProcessingSpinner();
513
- stopProcessingSpinner = null;
514
- }
515
- }
516
- prompt();
517
- });
518
- const introText =
519
- options.ui?.interactiveIntro ??
520
- `${options.appName} (${name} agent). Type your message or "exit" to quit.`;
521
- if (introText !== false) {
522
- console.log(`${introText}\n`);
523
- }
524
- prompt();
525
- }
526
-
527
- export function createStructuredRunEventListener(
528
- writer: (line: string) => void = console.error
529
- ): AgentEventListener {
530
- let step = 0;
531
- let lastCommandSignature = "";
532
- const stepActionByNumber = new Map<number, string>();
533
- const okMark = process.stderr.isTTY && !process.env.NO_COLOR ? "\x1b[32m✔\x1b[0m" : "✔";
534
-
535
- const asRecord = (value: unknown): Record<string, unknown> | null =>
536
- typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
537
-
538
- const asNumber = (value: unknown): number | null =>
539
- typeof value === "number" && Number.isFinite(value) ? value : null;
540
-
541
- const asString = (value: unknown): string | null =>
542
- typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
543
- const asAnyString = (value: unknown): string | null =>
544
- typeof value === "string" ? value : null;
545
-
546
- const parseJsonObject = (value: unknown): Record<string, unknown> | null => {
547
- if (typeof value !== "string") return null;
548
- try {
549
- const parsed = JSON.parse(value) as unknown;
550
- return asRecord(parsed);
551
- } catch {
552
- return null;
553
- }
554
- };
555
-
556
- const extractCommandMeta = (args: unknown) => {
557
- const rec = asRecord(args) ?? parseJsonObject(args);
558
- if (!rec) return null;
559
- const commandRaw = asAnyString(rec.command);
560
- if (commandRaw === null) return null;
561
- const command = commandRaw.trim();
562
- return {
563
- command,
564
- windowId: asNumber(rec.windowId),
565
- tabIndex: asNumber(rec.tabIndex),
566
- sessionId: asString(rec.sessionId),
567
- };
568
- };
569
-
570
- const isNoopCaptureCommand = (command: string): boolean => {
571
- const normalized = command.trim().replace(/\s+/g, " ");
572
- return (
573
- normalized === "" ||
574
- normalized === ":" ||
575
- normalized === "true" ||
576
- normalized === "printf ''" ||
577
- normalized === "printf \"\""
578
- );
579
- };
580
-
581
- const extractToolResultMeta = (payload: unknown) => {
582
- const payloadRec = asRecord(payload);
583
- const rawResult = payloadRec ? (payloadRec.result ?? null) : null;
584
- const resultRec = asRecord(rawResult) ?? parseJsonObject(rawResult);
585
- const nestedResult = resultRec ? asRecord(resultRec.result) : null;
586
- const output =
587
- asString((nestedResult ?? resultRec ?? {}).output) ??
588
- asString((nestedResult ?? resultRec ?? {}).result as unknown);
589
- const outputLines = output ? output.split(/\r?\n/).filter((line) => line.length > 0).length : null;
590
- const error = asString((nestedResult ?? resultRec ?? {}).error);
591
- return { outputLines, error };
592
- };
593
-
594
- return (event: AgentEvent) => {
595
- switch (event.name) {
596
- case "agent.react.run.start":
597
- case "agent.deep.run.start":
598
- step = 0;
599
- lastCommandSignature = "";
600
- stepActionByNumber.clear();
601
- writer("Analyzing started");
602
- return;
603
- case "agent.react.skill.matched": {
604
- const payload = (event.payload ?? {}) as { skill?: string; score?: number };
605
- const score = typeof payload.score === "number" ? payload.score.toFixed(3) : "?";
606
- writer(`[skill] matched ${payload.skill ?? "unknown"} (score ${score})`);
607
- return;
608
- }
609
- case "agent.react.tool.invoke.start": {
610
- step += 1;
611
- const payload = (event.payload ?? {}) as { args?: unknown };
612
- const commandMeta = extractCommandMeta(payload.args);
613
- if (commandMeta) {
614
- const signature = `${commandMeta.command}|${commandMeta.windowId ?? ""}|${commandMeta.tabIndex ?? ""}|${commandMeta.sessionId ?? ""}`;
615
- const isRepeat = signature === lastCommandSignature;
616
- lastCommandSignature = signature;
617
- const action = isNoopCaptureCommand(commandMeta.command)
618
- ? "capture current screen"
619
- : commandMeta.command;
620
- stepActionByNumber.set(step, `${action}${isRepeat ? " (repeat)" : ""}`);
621
- } else {
622
- stepActionByNumber.set(step, `tool: ${event.to}`);
623
- }
624
- return;
625
- }
626
- case "agent.react.tool.invoke.done": {
627
- const resultMeta = extractToolResultMeta(event.payload);
628
- const action = stepActionByNumber.get(step) ?? `tool: ${event.to}`;
629
- if (resultMeta.error) {
630
- writer(`[step ${step}] ${action} ✖ (${resultMeta.error})`);
631
- stepActionByNumber.delete(step);
632
- return;
633
- }
634
- writer(`[step ${step}] ${action} ${okMark}`);
635
- stepActionByNumber.delete(step);
636
- return;
637
- }
638
- case "agent.react.tool.invoke.error": {
639
- const payload = (event.payload ?? {}) as { error?: unknown };
640
- const action = stepActionByNumber.get(step) ?? `tool: ${event.to}`;
641
- const message =
642
- typeof payload.error === "string"
643
- ? payload.error
644
- : payload.error instanceof Error
645
- ? payload.error.message
646
- : "unknown error";
647
- writer(`[step ${step}] ${action} ✖ (${message})`);
648
- stepActionByNumber.delete(step);
649
- return;
650
- }
651
- case "agent.react.run.done":
652
- case "agent.deep.run.done":
653
- writer("completed");
654
- return;
655
- default:
656
- return;
657
- }
658
- };
108
+ console.log(renderForTerminal(text, { renderMarkdown, useColor }));
659
109
  }
660
110
 
661
111
  export function runAppCli(options: AppCliOptions): void {
662
- const { appName, createBotContext } = options;
663
-
664
- async function main() {
665
- const { kind, query } = parseArgs();
666
- const agentLabel = kind === REACT ? "ReAct (LangChain)" : "Deep (DeepAgents)";
667
- const loadingText =
668
- options.ui?.loadingText ?? `${appName}: loading config, LLM, memory, tools...`;
669
- const useLoadingSpinner = options.ui?.loadingSpinner ?? false;
670
- let stopLoadingSpinner: (() => void) | null = null;
671
- if (loadingText !== false) {
672
- if (useLoadingSpinner && Boolean(process.stderr.isTTY)) {
673
- stopLoadingSpinner = startLoadingSpinner(loadingText);
674
- } else {
675
- console.error(loadingText);
676
- }
677
- }
678
- const ctx = await createBotContext();
679
- if (stopLoadingSpinner) {
680
- stopLoadingSpinner();
681
- }
682
- let didCleanup = false;
683
- const cleanup = () => {
684
- if (didCleanup) return;
685
- didCleanup = true;
686
- try {
687
- options.onShutdown?.(ctx);
688
- } catch (err) {
689
- console.error(
690
- `${appName}: onShutdown hook failed:`,
691
- err instanceof Error ? err.message : err,
692
- );
693
- }
694
- };
695
- const exitApp = (code: number): never => {
696
- cleanup();
697
- process.exit(code);
698
- };
699
- process.once("exit", cleanup);
700
- process.once("SIGINT", () => exitApp(130));
701
- process.once("SIGTERM", () => exitApp(143));
112
+ const main = async () => {
113
+ const parsed = parseArgs();
114
+ const ctx = await createContextWithLoading(options);
115
+ const kind = resolveAgentKind(parsed, ctx);
116
+ const exitApp = createExitHandler(options.appName, options.onShutdown, ctx);
702
117
 
703
- if (process.env.AGENT_EVENT_STDERR === "1") {
704
- ctx.events.subscribe(createConsoleAgentEventListener());
705
- }
706
- const eventListeners = Array.isArray(options.eventListener)
707
- ? options.eventListener
708
- : options.eventListener
709
- ? [options.eventListener]
710
- : [];
711
- for (const listener of eventListeners) {
712
- ctx.events.subscribe(listener);
713
- }
714
- if (options.onReady) {
715
- try {
716
- await options.onReady(ctx);
717
- } catch (err) {
718
- console.error(
719
- `${appName}: onReady hook failed:`,
720
- err instanceof Error ? err.message : err,
721
- );
722
- }
723
- }
724
- const readyText = options.ui?.readyText ?? `Ready. Agent: ${agentLabel}`;
725
- if (readyText !== false) {
726
- console.error(readyText);
727
- }
118
+ subscribeEventListeners(options, ctx);
119
+ await runOnReady(options, options.appName, ctx);
120
+ printReady(options, kind);
728
121
 
729
- if (query) {
730
- const text = await runOne(ctx, kind, query);
731
- const useColor = options.ui?.useColor ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
732
- const renderMarkdown = options.ui?.renderMarkdown ?? true;
733
- console.log(renderForTerminal(text, { renderMarkdown, useColor }));
734
- cleanup();
735
- return;
122
+ if (parsed.query) {
123
+ await runSingleQuery(options, ctx, kind, parsed.query);
124
+ exitApp(0);
736
125
  }
737
126
 
738
127
  await interactive(ctx, kind, options, exitApp);
739
- }
128
+ };
740
129
 
741
130
  main().catch((err) => {
742
131
  console.error(err);