@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.
- package/.github/workflows/ci.yml +9 -24
- package/.github/workflows/release.yml +14 -35
- package/agent-runtime/.github/workflows/ci.yml +69 -0
- package/agent-runtime/.github/workflows/release.yml +118 -0
- package/agent-runtime/.releaserc.cjs +26 -0
- package/agent-runtime/config/agent.deep.yaml +25 -0
- package/agent-runtime/config/agent.react.yaml +24 -0
- package/agent-runtime/example/basic-usage.ts +49 -0
- package/agent-runtime/package-lock.json +7740 -0
- package/agent-runtime/package.json +49 -0
- package/agent-runtime/pnpm-lock.yaml +3712 -0
- package/agent-runtime/scripts/resolve-deps.js +54 -0
- package/agent-runtime/src/agents/deep-agent.ts +165 -0
- package/agent-runtime/src/agents/react-agent.helpers.ts +227 -0
- package/agent-runtime/src/agents/react-agent.ts +584 -0
- package/{src → agent-runtime/src/agents}/sub-agent.ts +2 -2
- package/agent-runtime/src/cli/args.ts +15 -0
- package/agent-runtime/src/cli/event-listener.ts +162 -0
- package/agent-runtime/src/cli/interactive.ts +144 -0
- package/agent-runtime/src/cli/runtime.ts +31 -0
- package/agent-runtime/src/cli/spinner.ts +23 -0
- package/agent-runtime/src/cli/terminal-render.ts +322 -0
- package/agent-runtime/src/cli/types.ts +33 -0
- package/agent-runtime/src/cli.ts +134 -0
- package/agent-runtime/src/config/helpers.ts +179 -0
- package/agent-runtime/src/config/index.ts +245 -0
- package/agent-runtime/src/config/types.ts +62 -0
- package/agent-runtime/src/core/context.ts +266 -0
- package/agent-runtime/src/index.ts +55 -0
- package/agent-runtime/tsconfig.json +18 -0
- package/apps/imessagebot/README.md +38 -0
- package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/README.md +33 -0
- package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/package-lock.json +15257 -0
- package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/package.json +55 -0
- package/apps/imessagebot/config/agents/deep/agent.yaml +31 -0
- package/apps/imessagebot/config/agents/react/agent.yaml +58 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/README.md +33 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/package-lock.json +15457 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/package.json +55 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/README.md +33 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/package-lock.json +15257 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/package.json +62 -0
- package/apps/imessagebot/config/agents/shared/memory.yaml +31 -0
- package/apps/imessagebot/config/agents/shared/model.yaml +23 -0
- package/apps/imessagebot/config/agents/shared/tool.yaml +13 -0
- package/apps/imessagebot/config/app.yaml +14 -0
- package/apps/imessagebot/package-lock.json +53695 -0
- package/apps/imessagebot/package.json +41 -0
- package/apps/imessagebot/pnpm-lock.yaml +1589 -0
- package/apps/imessagebot/scripts/resolve-deps.js +41 -0
- package/apps/imessagebot/scripts/test-llm.mjs +27 -0
- package/apps/imessagebot/scripts/validate-tools-config.mjs +174 -0
- package/apps/imessagebot/src/config.ts +76 -0
- package/apps/imessagebot/src/context.ts +35 -0
- package/apps/imessagebot/src/index.ts +17 -0
- package/apps/imessagebot/tsconfig.json +18 -0
- package/apps/itermbot/.github/workflows/ci.yml +61 -0
- package/apps/itermbot/.github/workflows/release.yml +80 -0
- package/apps/itermbot/.releaserc.cjs +26 -0
- package/apps/itermbot/README.md +82 -0
- package/apps/itermbot/config/app.yaml +29 -0
- package/apps/itermbot/config/tsconfig.json +18 -0
- package/apps/itermbot/macos_disk_usage_agent_plan.md +244 -0
- package/apps/itermbot/package-lock.json +53697 -0
- package/apps/itermbot/package.json +57 -0
- package/apps/itermbot/pnpm-lock.yaml +3966 -0
- package/apps/itermbot/scripts/patch-buildin-cache.sh +25 -0
- package/apps/itermbot/scripts/resolve-deps.js +41 -0
- package/apps/itermbot/scripts/test-llm.mjs +32 -0
- package/apps/itermbot/skills/command-explain-and-guard/SKILL.md +39 -0
- package/apps/itermbot/skills/command-explain-and-guard/handler.js +86 -0
- package/apps/itermbot/skills/disk-usage-investigate/SKILL.md +44 -0
- package/apps/itermbot/skills/disk-usage-investigate/handler.js +12 -0
- package/apps/itermbot/skills/gpu-ssh-monitor/SKILL.md +64 -0
- package/apps/itermbot/skills/repo-triage/SKILL.md +40 -0
- package/apps/itermbot/skills/repo-triage/handler.js +56 -0
- package/apps/itermbot/skills/test-failure-diagnose/SKILL.md +43 -0
- package/apps/itermbot/skills/test-failure-diagnose/handler.js +107 -0
- package/apps/itermbot/src/config.ts +95 -0
- package/apps/itermbot/src/context.ts +35 -0
- package/apps/itermbot/src/index.ts +223 -0
- package/apps/itermbot/src/iterm/session-hint.ts +40 -0
- package/apps/itermbot/src/iterm/target-routing.ts +419 -0
- package/apps/itermbot/src/startup/colors.ts +317 -0
- package/apps/itermbot/src/startup/diagnostics.ts +97 -0
- package/apps/itermbot/src/startup/ui.ts +141 -0
- package/config/agent.deep.yaml +25 -0
- package/config/agent.react.yaml +24 -0
- package/dist/agents/deep-agent.d.ts +37 -0
- package/dist/agents/deep-agent.d.ts.map +1 -0
- package/dist/agents/deep-agent.js +115 -0
- package/dist/agents/deep-agent.js.map +1 -0
- package/dist/agents/react-agent.d.ts +40 -0
- package/dist/agents/react-agent.d.ts.map +1 -0
- package/dist/agents/react-agent.helpers.d.ts +40 -0
- package/dist/agents/react-agent.helpers.d.ts.map +1 -0
- package/dist/agents/react-agent.helpers.js +196 -0
- package/dist/agents/react-agent.helpers.js.map +1 -0
- package/dist/agents/react-agent.js +400 -0
- package/dist/agents/react-agent.js.map +1 -0
- package/dist/agents/sub-agent.d.ts +34 -0
- package/dist/agents/sub-agent.d.ts.map +1 -0
- package/dist/agents/sub-agent.js +53 -0
- package/dist/agents/sub-agent.js.map +1 -0
- package/dist/cli/args.d.ts +8 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +9 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/event-listener.d.ts +3 -0
- package/dist/cli/event-listener.d.ts.map +1 -0
- package/dist/cli/event-listener.js +131 -0
- package/dist/cli/event-listener.js.map +1 -0
- package/dist/cli/interactive.d.ts +4 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +118 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/runtime.d.ts +8 -0
- package/dist/cli/runtime.d.ts.map +1 -0
- package/dist/cli/runtime.js +27 -0
- package/dist/cli/runtime.js.map +1 -0
- package/dist/cli/spinner.d.ts +2 -0
- package/dist/cli/spinner.d.ts.map +1 -0
- package/dist/cli/spinner.js +22 -0
- package/dist/cli/spinner.js.map +1 -0
- package/dist/cli/terminal-render.d.ts +7 -0
- package/dist/cli/terminal-render.d.ts.map +1 -0
- package/dist/cli/terminal-render.js +282 -0
- package/dist/cli/terminal-render.js.map +1 -0
- package/dist/cli/types.d.ts +29 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +3 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli.d.ts +4 -41
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -588
- package/dist/cli.js.map +1 -1
- package/dist/config/helpers.d.ts +6 -0
- package/dist/config/helpers.d.ts.map +1 -0
- package/dist/config/helpers.js +164 -0
- package/dist/config/helpers.js.map +1 -0
- package/dist/config/index.d.ts +15 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +160 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +57 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/context.d.ts +8 -69
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +44 -24
- package/dist/context.js.map +1 -1
- package/dist/core/context.d.ts +66 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +149 -0
- package/dist/core/context.js.map +1 -0
- package/dist/deep-agent.d.ts +5 -2
- package/dist/deep-agent.d.ts.map +1 -1
- package/dist/deep-agent.js +44 -11
- package/dist/deep-agent.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/middleware/malformed-tool-call-middleware.d.ts +8 -0
- package/dist/middleware/malformed-tool-call-middleware.d.ts.map +1 -0
- package/dist/middleware/malformed-tool-call-middleware.js +191 -0
- package/dist/middleware/malformed-tool-call-middleware.js.map +1 -0
- package/dist/react-agent.d.ts +2 -2
- package/dist/react-agent.d.ts.map +1 -1
- package/dist/react-agent.js +28 -9
- package/dist/react-agent.js.map +1 -1
- package/package.json +1 -1
- package/scripts/resolve-deps.js +54 -0
- package/src/agents/deep-agent.ts +165 -0
- package/src/agents/react-agent.helpers.ts +227 -0
- package/src/agents/react-agent.ts +584 -0
- package/src/agents/sub-agent.ts +82 -0
- package/src/cli/args.ts +15 -0
- package/src/cli/event-listener.ts +162 -0
- package/src/cli/interactive.ts +144 -0
- package/src/cli/runtime.ts +31 -0
- package/src/cli/spinner.ts +23 -0
- package/src/cli/terminal-render.ts +322 -0
- package/src/cli/types.ts +33 -0
- package/src/cli.ts +91 -702
- package/src/config/helpers.ts +179 -0
- package/src/config/index.ts +245 -0
- package/src/config/types.ts +62 -0
- package/src/core/context.ts +266 -0
- package/src/index.ts +13 -11
- package/src/middleware/malformed-tool-call-middleware.ts +239 -0
- package/src/types/markdown-it-terminal.d.ts +4 -0
- package/src/types/marked-terminal.d.ts +16 -0
- package/dist/config.d.ts +0 -86
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -84
- package/dist/config.js.map +0 -1
- package/src/config.ts +0 -177
- package/src/context.ts +0 -247
- package/src/deep-agent.ts +0 -104
- package/src/react-agent.ts +0 -576
- /package/{src → agent-runtime/src/middleware}/malformed-tool-call-middleware.ts +0 -0
- /package/{src → agent-runtime/src/types}/markdown-it-terminal.d.ts +0 -0
- /package/{src → agent-runtime/src/types}/marked-terminal.d.ts +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { AgentEvent, AgentEventListener } from "@easynet/agent-common";
|
|
2
|
+
|
|
3
|
+
interface CommandMeta {
|
|
4
|
+
command: string;
|
|
5
|
+
windowId: number | null;
|
|
6
|
+
tabIndex: number | null;
|
|
7
|
+
sessionId: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ToolResultMeta {
|
|
11
|
+
outputLines: number | null;
|
|
12
|
+
error: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ListenerState {
|
|
16
|
+
step: number;
|
|
17
|
+
lastCommandSignature: string;
|
|
18
|
+
stepActionByNumber: Map<number, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
22
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function asNumber(value: unknown): number | null {
|
|
26
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function asTrimmedString(value: unknown): string | null {
|
|
30
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asString(value: unknown): string | null {
|
|
34
|
+
return typeof value === "string" ? value : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseJsonObject(value: unknown): Record<string, unknown> | null {
|
|
38
|
+
if (typeof value !== "string") return null;
|
|
39
|
+
try {
|
|
40
|
+
return asRecord(JSON.parse(value));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractCommandMeta(args: unknown): CommandMeta | null {
|
|
47
|
+
const rec = asRecord(args) ?? parseJsonObject(args);
|
|
48
|
+
if (!rec) return null;
|
|
49
|
+
const commandRaw = asString(rec.command);
|
|
50
|
+
if (commandRaw === null) return null;
|
|
51
|
+
return {
|
|
52
|
+
command: commandRaw.trim(),
|
|
53
|
+
windowId: asNumber(rec.windowId),
|
|
54
|
+
tabIndex: asNumber(rec.tabIndex),
|
|
55
|
+
sessionId: asTrimmedString(rec.sessionId),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isNoopCaptureCommand(command: string): boolean {
|
|
60
|
+
const normalized = command.trim().replace(/\s+/g, " ");
|
|
61
|
+
return normalized === "" || normalized === ":" || normalized === "true" || normalized === "printf ''" || normalized === 'printf ""';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function extractToolResultMeta(payload: unknown): ToolResultMeta {
|
|
65
|
+
const payloadRec = asRecord(payload);
|
|
66
|
+
const rawResult = payloadRec ? (payloadRec.result ?? null) : null;
|
|
67
|
+
const resultRec = asRecord(rawResult) ?? parseJsonObject(rawResult);
|
|
68
|
+
const nestedResult = resultRec ? asRecord(resultRec.result) : null;
|
|
69
|
+
const output = asTrimmedString((nestedResult ?? resultRec ?? {}).output)
|
|
70
|
+
?? asTrimmedString((nestedResult ?? resultRec ?? {}).result as unknown);
|
|
71
|
+
const error = asTrimmedString((nestedResult ?? resultRec ?? {}).error);
|
|
72
|
+
return {
|
|
73
|
+
outputLines: output ? output.split(/\r?\n/).filter((line) => line.length > 0).length : null,
|
|
74
|
+
error,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function recordToolStart(state: ListenerState, event: AgentEvent): void {
|
|
79
|
+
state.step += 1;
|
|
80
|
+
const payload = (event.payload ?? {}) as { args?: unknown };
|
|
81
|
+
const commandMeta = extractCommandMeta(payload.args);
|
|
82
|
+
|
|
83
|
+
if (!commandMeta) {
|
|
84
|
+
state.stepActionByNumber.set(state.step, `tool: ${event.to}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const signature = `${commandMeta.command}|${commandMeta.windowId ?? ""}|${commandMeta.tabIndex ?? ""}|${commandMeta.sessionId ?? ""}`;
|
|
89
|
+
const isRepeat = signature === state.lastCommandSignature;
|
|
90
|
+
state.lastCommandSignature = signature;
|
|
91
|
+
const action = isNoopCaptureCommand(commandMeta.command) ? "capture current screen" : commandMeta.command;
|
|
92
|
+
state.stepActionByNumber.set(state.step, `${action}${isRepeat ? " (repeat)" : ""}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeToolDone(state: ListenerState, event: AgentEvent, writer: (line: string) => void, okMark: string): void {
|
|
96
|
+
const resultMeta = extractToolResultMeta(event.payload);
|
|
97
|
+
const action = state.stepActionByNumber.get(state.step) ?? `tool: ${event.to}`;
|
|
98
|
+
|
|
99
|
+
if (resultMeta.error) {
|
|
100
|
+
writer(`[step ${state.step}] ${action} ✖ (${resultMeta.error})`);
|
|
101
|
+
state.stepActionByNumber.delete(state.step);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
writer(`[step ${state.step}] ${action} ${okMark}`);
|
|
106
|
+
state.stepActionByNumber.delete(state.step);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeToolError(state: ListenerState, event: AgentEvent, writer: (line: string) => void): void {
|
|
110
|
+
const payload = (event.payload ?? {}) as { error?: unknown };
|
|
111
|
+
const action = state.stepActionByNumber.get(state.step) ?? `tool: ${event.to}`;
|
|
112
|
+
const message = typeof payload.error === "string"
|
|
113
|
+
? payload.error
|
|
114
|
+
: payload.error instanceof Error
|
|
115
|
+
? payload.error.message
|
|
116
|
+
: "unknown error";
|
|
117
|
+
writer(`[step ${state.step}] ${action} ✖ (${message})`);
|
|
118
|
+
state.stepActionByNumber.delete(state.step);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleEvent(state: ListenerState, event: AgentEvent, writer: (line: string) => void, okMark: string): void {
|
|
122
|
+
switch (event.name) {
|
|
123
|
+
case "agent.react.run.start":
|
|
124
|
+
case "agent.deep.run.start":
|
|
125
|
+
state.step = 0;
|
|
126
|
+
state.lastCommandSignature = "";
|
|
127
|
+
state.stepActionByNumber.clear();
|
|
128
|
+
writer("Analyzing started");
|
|
129
|
+
return;
|
|
130
|
+
case "agent.react.skill.matched": {
|
|
131
|
+
const payload = (event.payload ?? {}) as { skill?: string; score?: number };
|
|
132
|
+
const score = typeof payload.score === "number" ? payload.score.toFixed(3) : "?";
|
|
133
|
+
writer(`[skill] matched ${payload.skill ?? "unknown"} (score ${score})`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
case "agent.react.tool.invoke.start":
|
|
137
|
+
recordToolStart(state, event);
|
|
138
|
+
return;
|
|
139
|
+
case "agent.react.tool.invoke.done":
|
|
140
|
+
writeToolDone(state, event, writer, okMark);
|
|
141
|
+
return;
|
|
142
|
+
case "agent.react.tool.invoke.error":
|
|
143
|
+
writeToolError(state, event, writer);
|
|
144
|
+
return;
|
|
145
|
+
case "agent.react.run.done":
|
|
146
|
+
case "agent.deep.run.done":
|
|
147
|
+
writer("completed");
|
|
148
|
+
return;
|
|
149
|
+
default:
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createStructuredRunEventListener(writer: (line: string) => void = console.error): AgentEventListener {
|
|
155
|
+
const state: ListenerState = {
|
|
156
|
+
step: 0,
|
|
157
|
+
lastCommandSignature: "",
|
|
158
|
+
stepActionByNumber: new Map<number, string>(),
|
|
159
|
+
};
|
|
160
|
+
const okMark = process.stderr.isTTY && !process.env.NO_COLOR ? "\x1b[32m✔\x1b[0m" : "✔";
|
|
161
|
+
return (event: AgentEvent) => handleEvent(state, event, writer, okMark);
|
|
162
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import type { BotContext } from "../core/context.js";
|
|
4
|
+
import { createRuntime } from "./runtime.js";
|
|
5
|
+
import { startLoadingSpinner } from "./spinner.js";
|
|
6
|
+
import { renderForTerminal } from "./terminal-render.js";
|
|
7
|
+
import { DEEP, REACT, type AgentKind, type AppCliOptions, type InteractiveCommandHandler } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const builtinInteractiveCommands: Record<string, InteractiveCommandHandler> = {
|
|
10
|
+
"list tools": (ctx) => {
|
|
11
|
+
const tools = (ctx.tools as Array<{ name?: unknown; description?: unknown }>)
|
|
12
|
+
.map((tool) => ({
|
|
13
|
+
name: typeof tool.name === "string" ? tool.name : "(unnamed-tool)",
|
|
14
|
+
description:
|
|
15
|
+
typeof tool.description === "string" && tool.description.trim().length > 0
|
|
16
|
+
? tool.description.trim()
|
|
17
|
+
: "No description",
|
|
18
|
+
}))
|
|
19
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
20
|
+
|
|
21
|
+
console.log(`Available tools (${tools.length}):`);
|
|
22
|
+
for (const tool of tools) {
|
|
23
|
+
console.log(`- ${tool.name}: ${tool.description}`);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"list skills": (ctx) => {
|
|
27
|
+
const skills = (ctx.skillSet?.list() ?? [])
|
|
28
|
+
.map((skill) => ({ name: skill.name, description: (skill.description ?? "").trim() || "No description" }))
|
|
29
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
30
|
+
|
|
31
|
+
console.log(`Available skills (${skills.length}):`);
|
|
32
|
+
for (const skill of skills) {
|
|
33
|
+
console.log(`- ${skill.name}: ${skill.description}`);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function escapeRegExp(value: string): string {
|
|
39
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createCliColors(useColor: boolean) {
|
|
43
|
+
return {
|
|
44
|
+
reset: useColor ? "\x1b[0m" : "",
|
|
45
|
+
dim: useColor ? "\x1b[2m" : "",
|
|
46
|
+
user: useColor ? "\x1b[38;5;39m" : "",
|
|
47
|
+
bot: useColor ? "\x1b[38;5;48m" : "",
|
|
48
|
+
prompt: useColor ? "\x1b[38;5;245m" : "",
|
|
49
|
+
promptUser: useColor ? "\x1b[1;38;5;45m" : "",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveUiOptions(kind: AgentKind, options: AppCliOptions) {
|
|
54
|
+
const userLabel = options.ui?.userLabel ?? os.userInfo().username;
|
|
55
|
+
const assistantLabel = options.ui?.assistantLabel ?? (kind === REACT ? "ReAct" : "Deep");
|
|
56
|
+
const useColor = options.ui?.useColor ?? (Boolean(process.stdout.isTTY) && !process.env.NO_COLOR);
|
|
57
|
+
const renderMarkdown = options.ui?.renderMarkdown ?? true;
|
|
58
|
+
const echoUserQuestion = options.ui?.echoUserQuestion ?? true;
|
|
59
|
+
const showProcessingSpinner = options.ui?.processingSpinner ?? Boolean(process.stderr.isTTY);
|
|
60
|
+
const processingText = options.ui?.processingText ?? "Processing";
|
|
61
|
+
return { userLabel, assistantLabel, useColor, renderMarkdown, echoUserQuestion, showProcessingSpinner, processingText };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runInteractiveCommand(
|
|
65
|
+
ctx: BotContext,
|
|
66
|
+
input: string,
|
|
67
|
+
commands: Record<string, InteractiveCommandHandler> | undefined,
|
|
68
|
+
): Promise<boolean> {
|
|
69
|
+
const key = input.toLowerCase();
|
|
70
|
+
const handler = commands?.[key] ?? builtinInteractiveCommands[key];
|
|
71
|
+
if (!handler) return false;
|
|
72
|
+
await handler(ctx);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function runUserQuery(
|
|
77
|
+
query: string,
|
|
78
|
+
runtime: { run: (userMessage: string) => Promise<{ text: string }> },
|
|
79
|
+
ui: ReturnType<typeof resolveUiOptions>,
|
|
80
|
+
visuals: { hr: string; userPrefix: string; assistantPrefix: string },
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
let stopProcessingSpinner: (() => void) | null = null;
|
|
83
|
+
try {
|
|
84
|
+
console.log(`\n${visuals.hr}`);
|
|
85
|
+
if (ui.echoUserQuestion) {
|
|
86
|
+
console.log(visuals.userPrefix);
|
|
87
|
+
console.log(`> ${query}`);
|
|
88
|
+
console.log("");
|
|
89
|
+
}
|
|
90
|
+
console.log(visuals.assistantPrefix);
|
|
91
|
+
stopProcessingSpinner = ui.showProcessingSpinner
|
|
92
|
+
? startLoadingSpinner(ui.processingText === false ? "⏳" : `⏳ ${ui.processingText}`)
|
|
93
|
+
: null;
|
|
94
|
+
|
|
95
|
+
const { text } = await runtime.run(query);
|
|
96
|
+
stopProcessingSpinner?.();
|
|
97
|
+
stopProcessingSpinner = null;
|
|
98
|
+
|
|
99
|
+
console.log(renderForTerminal(text, { renderMarkdown: ui.renderMarkdown, useColor: ui.useColor }));
|
|
100
|
+
console.log(`${visuals.hr}\n`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
stopProcessingSpinner?.();
|
|
103
|
+
console.error("Error:", err instanceof Error ? err.message : err);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function interactive(
|
|
108
|
+
ctx: BotContext,
|
|
109
|
+
kind: AgentKind,
|
|
110
|
+
options: AppCliOptions,
|
|
111
|
+
exitApp: (code: number) => never,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const runtime = await createRuntime(ctx, kind);
|
|
114
|
+
const ui = resolveUiOptions(kind, options);
|
|
115
|
+
const color = createCliColors(ui.useColor);
|
|
116
|
+
const hr = `${color.dim}${"-".repeat(56)}${color.reset}`;
|
|
117
|
+
const promptText = `${color.prompt}[${color.promptUser}${ui.userLabel}${color.reset}${color.prompt}]${color.reset} `;
|
|
118
|
+
const userPrefix = `${color.user}[${ui.userLabel}]${color.reset}`;
|
|
119
|
+
const assistantPrefix = `${color.bot}[${ui.assistantLabel}]${color.reset}`;
|
|
120
|
+
const userLabelPrefixPattern = new RegExp(`^(?:\\[${escapeRegExp(ui.userLabel)}\\]\\s*)+`, "i");
|
|
121
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
122
|
+
|
|
123
|
+
const introText = options.ui?.interactiveIntro ?? `${options.appName} (${kind === DEEP ? "Deep" : "ReAct"} agent). Type your message or "exit" to quit.`;
|
|
124
|
+
if (introText !== false) console.log(`${introText}\n`);
|
|
125
|
+
|
|
126
|
+
const prompt = () => {
|
|
127
|
+
rl.question(promptText, async (line) => {
|
|
128
|
+
const query = line?.trim()?.replace(userLabelPrefixPattern, "").trim();
|
|
129
|
+
if (!query) return prompt();
|
|
130
|
+
if (query === "exit" || query === "quit") {
|
|
131
|
+
rl.close();
|
|
132
|
+
exitApp(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handled = await runInteractiveCommand(ctx, query, options.interactiveCommands);
|
|
136
|
+
if (handled) return prompt();
|
|
137
|
+
|
|
138
|
+
await runUserQuery(query, runtime, ui, { hr, userPrefix, assistantPrefix });
|
|
139
|
+
prompt();
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
prompt();
|
|
144
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createReactAgent, type ReactAgentRuntime } from "../agents/react-agent.js";
|
|
2
|
+
import { createDeepAgent, type DeepAgentRuntime } from "../agents/deep-agent.js";
|
|
3
|
+
import { malformedToolCallMiddleware } from "../middleware/malformed-tool-call-middleware.js";
|
|
4
|
+
import type { BotContext } from "../core/context.js";
|
|
5
|
+
import { DEEP, REACT, type AgentKind } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export function readDefaultAgentKindFromConfig(ctx: BotContext): AgentKind | undefined {
|
|
8
|
+
const defaultAgent = (ctx.config?.app as { defaultAgent?: unknown } | undefined)?.defaultAgent;
|
|
9
|
+
if (typeof defaultAgent !== "string") return undefined;
|
|
10
|
+
const value = defaultAgent.trim().toLowerCase();
|
|
11
|
+
if (value === REACT || value === DEEP) return value;
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function createRuntime(
|
|
16
|
+
ctx: BotContext,
|
|
17
|
+
kind: AgentKind,
|
|
18
|
+
): Promise<ReactAgentRuntime | DeepAgentRuntime> {
|
|
19
|
+
if (kind === REACT) {
|
|
20
|
+
return createReactAgent(ctx, {
|
|
21
|
+
middleware: [malformedToolCallMiddleware()],
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return createDeepAgent(ctx);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runOne(ctx: BotContext, kind: AgentKind, query: string): Promise<string> {
|
|
28
|
+
const runtime = await createRuntime(ctx, kind);
|
|
29
|
+
const { text } = await runtime.run(query);
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function startLoadingSpinner(message: string): () => void {
|
|
2
|
+
const frames = ["", ".", "..", "..."];
|
|
3
|
+
let frameIndex = 0;
|
|
4
|
+
let lastLength = 0;
|
|
5
|
+
|
|
6
|
+
const render = () => {
|
|
7
|
+
const frame = frames[frameIndex % frames.length] ?? "";
|
|
8
|
+
const trimmed = message.trim();
|
|
9
|
+
const base = trimmed.startsWith("⏳") ? trimmed : `⏳ ${trimmed}`;
|
|
10
|
+
const line = `${base}${frame}`;
|
|
11
|
+
const padded = lastLength > line.length ? line.padEnd(lastLength, " ") : line;
|
|
12
|
+
process.stderr.write(`\r${padded}\r`);
|
|
13
|
+
lastLength = padded.length;
|
|
14
|
+
frameIndex += 1;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
render();
|
|
18
|
+
const timer = setInterval(render, 90);
|
|
19
|
+
return () => {
|
|
20
|
+
clearInterval(timer);
|
|
21
|
+
process.stderr.write(`\r${" ".repeat(lastLength)}\r`);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
import markdownItTerminal from "markdown-it-terminal";
|
|
3
|
+
|
|
4
|
+
interface RenderOptions {
|
|
5
|
+
renderMarkdown: boolean;
|
|
6
|
+
useColor: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface FallbackRenderOptions {
|
|
10
|
+
useColor: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type AnsiColors = Record<string, string>;
|
|
14
|
+
|
|
15
|
+
const markdownRenderers = new Map<"color" | "plain", MarkdownIt>();
|
|
16
|
+
|
|
17
|
+
function createAnsi(useColor: boolean): AnsiColors {
|
|
18
|
+
return {
|
|
19
|
+
reset: useColor ? "\x1b[0m" : "",
|
|
20
|
+
dim: useColor ? "\x1b[2m" : "",
|
|
21
|
+
heading: useColor ? "\x1b[1;38;5;45m" : "",
|
|
22
|
+
bullet: useColor ? "\x1b[38;5;81m" : "",
|
|
23
|
+
code: useColor ? "\x1b[38;5;221m" : "",
|
|
24
|
+
quote: useColor ? "\x1b[38;5;245m" : "",
|
|
25
|
+
hr: useColor ? "\x1b[38;5;240m" : "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function styleInlineMarkdown(line: string, ansi: AnsiColors): string {
|
|
30
|
+
return line
|
|
31
|
+
.replace(/`([^`]+)`/g, `${ansi.code}\`$1\`${ansi.reset}`)
|
|
32
|
+
.replace(/\*\*([^*]+)\*\*/g, `${ansi.heading}$1${ansi.reset}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseTableRow(line: string): string[] {
|
|
36
|
+
const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
37
|
+
return trimmed.split("|").map((cell) => cell.trim());
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTableSeparatorRow(cells: string[]): boolean {
|
|
41
|
+
return cells.length > 0 && cells.every((c) => /^:?-{3,}:?$/.test(c));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderTable(rows: string[][], ansi: AnsiColors): string[] {
|
|
45
|
+
const normalized = rows.map((row) => [...row]);
|
|
46
|
+
const colCount = Math.max(...normalized.map((row) => row.length));
|
|
47
|
+
for (const row of normalized) {
|
|
48
|
+
while (row.length < colCount) row.push("");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const widths = Array.from({ length: colCount }, (_, i) =>
|
|
52
|
+
Math.max(...normalized.map((row) => (row[i] ?? "").length)),
|
|
53
|
+
);
|
|
54
|
+
const border = `+${widths.map((w) => "-".repeat(w + 2)).join("+")}+`;
|
|
55
|
+
const output: string[] = [`${ansi.hr}${border}${ansi.reset}`];
|
|
56
|
+
|
|
57
|
+
normalized.forEach((row, idx) => {
|
|
58
|
+
const body = row.map((cell, i) => ` ${(cell ?? "").padEnd(widths[i] ?? 0)} `).join("|");
|
|
59
|
+
const styled = idx === 0
|
|
60
|
+
? `${ansi.heading}|${styleInlineMarkdown(body, ansi)}|${ansi.reset}`
|
|
61
|
+
: `|${styleInlineMarkdown(body, ansi)}|`;
|
|
62
|
+
output.push(styled);
|
|
63
|
+
if (idx === 0) output.push(`${ansi.hr}${border}${ansi.reset}`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
output.push(`${ansi.hr}${border}${ansi.reset}`);
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function appendCodeFenceLine(line: string, state: { inCodeBlock: boolean; codeBoxWidth: number; }, ansi: AnsiColors, out: string[]): void {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
if (!trimmed.startsWith("```")) return;
|
|
73
|
+
|
|
74
|
+
if (!state.inCodeBlock) {
|
|
75
|
+
const lang = trimmed.slice(3).trim();
|
|
76
|
+
const title = lang ? ` code:${lang} ` : " code ";
|
|
77
|
+
state.codeBoxWidth = Math.max(16, title.length + 4);
|
|
78
|
+
state.inCodeBlock = true;
|
|
79
|
+
out.push(`${ansi.hr}┌${"─".repeat(state.codeBoxWidth)}┐${ansi.reset}`);
|
|
80
|
+
out.push(`${ansi.hr}│${ansi.reset}${ansi.dim}${title.padEnd(state.codeBoxWidth)}${ansi.reset}${ansi.hr}│${ansi.reset}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
state.inCodeBlock = false;
|
|
85
|
+
out.push(`${ansi.hr}└${"─".repeat(state.codeBoxWidth)}┘${ansi.reset}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderMarkdownTable(lines: string[], index: number, ansi: AnsiColors): { nextIndex: number; rendered: string[] } | null {
|
|
89
|
+
const line = lines[index] ?? "";
|
|
90
|
+
const next = lines[index + 1] ?? "";
|
|
91
|
+
if (!line.includes("|") || !next.includes("|")) return null;
|
|
92
|
+
|
|
93
|
+
const header = parseTableRow(line);
|
|
94
|
+
const separator = parseTableRow(next);
|
|
95
|
+
if (!(header.length > 1 && header.length === separator.length && isTableSeparatorRow(separator))) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const rows: string[][] = [header];
|
|
100
|
+
let cursor = index + 2;
|
|
101
|
+
while (cursor < lines.length && (lines[cursor] ?? "").includes("|")) {
|
|
102
|
+
rows.push(parseTableRow(lines[cursor] ?? ""));
|
|
103
|
+
cursor += 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { nextIndex: cursor - 1, rendered: renderTable(rows, ansi) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatMarkdownForTerminal(markdown: string, options: FallbackRenderOptions = { useColor: true }): string {
|
|
110
|
+
const ansi = createAnsi(options.useColor);
|
|
111
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
112
|
+
const out: string[] = [];
|
|
113
|
+
const state = { inCodeBlock: false, codeBoxWidth: 0 };
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
116
|
+
const line = lines[i] ?? "";
|
|
117
|
+
const before = state.inCodeBlock;
|
|
118
|
+
appendCodeFenceLine(line, state, ansi, out);
|
|
119
|
+
if (line.trim().startsWith("```")) continue;
|
|
120
|
+
|
|
121
|
+
if (before && state.inCodeBlock) {
|
|
122
|
+
const content = line.length > state.codeBoxWidth ? line.slice(0, state.codeBoxWidth) : line.padEnd(state.codeBoxWidth);
|
|
123
|
+
out.push(`${ansi.hr}│${ansi.reset}${ansi.code}${content}${ansi.reset}${ansi.hr}│${ansi.reset}`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const table = renderMarkdownTable(lines, i, ansi);
|
|
128
|
+
if (table) {
|
|
129
|
+
out.push(...table.rendered);
|
|
130
|
+
i = table.nextIndex;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (/^\s*#{1,6}\s+/.test(line)) {
|
|
135
|
+
out.push(`${ansi.heading}${line.replace(/^\s*#{1,6}\s+/, "").trim()}${ansi.reset}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (/^\s*>\s?/.test(line)) {
|
|
139
|
+
out.push(`${ansi.quote}${line}${ansi.reset}`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (/^\s*([-*]|\d+\.)\s+/.test(line)) {
|
|
143
|
+
out.push(`${ansi.bullet}${styleInlineMarkdown(line, ansi)}${ansi.reset}`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (/^\s*---+\s*$/.test(line)) {
|
|
147
|
+
out.push(`${ansi.hr}${"─".repeat(56)}${ansi.reset}`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
out.push(styleInlineMarkdown(line, ansi));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return out.join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sanitizeLine(line: string): string {
|
|
158
|
+
let next = line;
|
|
159
|
+
const trimmed = next.trim();
|
|
160
|
+
if (next.includes("```") && !trimmed.startsWith("```")) {
|
|
161
|
+
next = next.replace(/```+/g, "").replace(/\s+$/, "");
|
|
162
|
+
}
|
|
163
|
+
if (/^\s*#{3,}\s+/.test(next)) {
|
|
164
|
+
next = next.replace(/^\s*#{3,}\s+/, "## ");
|
|
165
|
+
}
|
|
166
|
+
return next;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizeListLine(line: string): string {
|
|
170
|
+
if (/^(\s{2,}|\t+)([*-])\s+/.test(line)) {
|
|
171
|
+
return line.replace(/^(\s{2,}|\t+)([*-])\s+/, "- ");
|
|
172
|
+
}
|
|
173
|
+
if (/^(\s{2,}|\t+)(\d+\.)\s+/.test(line)) {
|
|
174
|
+
return line.replace(/^(\s{2,}|\t+)(\d+\.)\s+/, "$2 ");
|
|
175
|
+
}
|
|
176
|
+
return line;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function applyHeadingHeuristics(lines: string[], hasMarkdownHeadings: boolean): string[] {
|
|
180
|
+
return lines.map((line) => {
|
|
181
|
+
const trimmed = line.trim();
|
|
182
|
+
const bulletHeading = trimmed.match(/^[-*]\s+\*\*([^*]+)\*\*: ?\s*$/);
|
|
183
|
+
if (bulletHeading?.[1]) return `### ${bulletHeading[1].trim()}`;
|
|
184
|
+
|
|
185
|
+
const plainBullet = trimmed.match(/^[-*]\s+([^`].+):\s*$/);
|
|
186
|
+
if (plainBullet?.[1] && plainBullet[1].length <= 48) {
|
|
187
|
+
return `### ${plainBullet[1].trim()}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (hasMarkdownHeadings) return trimmed === "Summary (3‑8 bullets)" ? "## Summary" : line;
|
|
191
|
+
|
|
192
|
+
const unwrapped = trimmed.replace(/^\*\*(.+)\*\*$/, "$1").trim();
|
|
193
|
+
if (/^(Key terminal output.*|Current terminal buffer.*|Summary.*|Next steps.*)$/i.test(unwrapped)) {
|
|
194
|
+
return `## ${unwrapped}`;
|
|
195
|
+
}
|
|
196
|
+
if (/^(Key observations|Findings|Analysis|Conclusion)$/i.test(unwrapped)) {
|
|
197
|
+
return `## ${unwrapped}`;
|
|
198
|
+
}
|
|
199
|
+
return line;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function mergeWrappedListItems(lines: string[]): string[] {
|
|
204
|
+
const merged: string[] = [];
|
|
205
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
206
|
+
const current = lines[i] ?? "";
|
|
207
|
+
const next = lines[i + 1] ?? "";
|
|
208
|
+
const isListLine = /^([-*]|\d+\.)\s+/.test(current.trim());
|
|
209
|
+
const nextTrim = next.trim();
|
|
210
|
+
const nextStartsNewBlock =
|
|
211
|
+
nextTrim.length === 0 ||
|
|
212
|
+
/^([-*]|\d+\.)\s+/.test(nextTrim) ||
|
|
213
|
+
/^#{1,6}\s+/.test(nextTrim) ||
|
|
214
|
+
/^```/.test(nextTrim) ||
|
|
215
|
+
/^---+$/.test(nextTrim);
|
|
216
|
+
const wrappedContinuation = isListLine && !nextStartsNewBlock && /^[0-9./~]/.test(nextTrim);
|
|
217
|
+
|
|
218
|
+
if (wrappedContinuation) {
|
|
219
|
+
merged.push(`${current.replace(/\s+$/, "")} ${nextTrim}`);
|
|
220
|
+
i += 1;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
merged.push(current);
|
|
224
|
+
}
|
|
225
|
+
return merged;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function fenceIndentedOutputBlocks(lines: string[]): string[] {
|
|
229
|
+
const output: string[] = [];
|
|
230
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
231
|
+
const line = lines[i] ?? "";
|
|
232
|
+
output.push(line);
|
|
233
|
+
|
|
234
|
+
const heading = line.trim();
|
|
235
|
+
const isOutputHeading = /^##\s*(Key terminal output|Current terminal buffer|Terminal output)/i.test(heading);
|
|
236
|
+
if (!isOutputHeading) continue;
|
|
237
|
+
if ((lines[i + 1] ?? "").trim().startsWith("```")) continue;
|
|
238
|
+
|
|
239
|
+
let j = i + 1;
|
|
240
|
+
const block: string[] = [];
|
|
241
|
+
while (j < lines.length) {
|
|
242
|
+
const current = lines[j] ?? "";
|
|
243
|
+
if (!current.trim()) {
|
|
244
|
+
if (block.length === 0) {
|
|
245
|
+
j += 1;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
if (!/^\s{2,}\S/.test(current)) break;
|
|
251
|
+
block.push(current.replace(/^\s+/, ""));
|
|
252
|
+
j += 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (block.length >= 2) {
|
|
256
|
+
output.push("```text", ...block, "```");
|
|
257
|
+
i = j - 1;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return output;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function ensureBalancedCodeFences(lines: string[]): string[] {
|
|
264
|
+
const count = lines.reduce((acc, line) => acc + (/^\s*```/.test(line) ? 1 : 0), 0);
|
|
265
|
+
if (count % 2 === 0) return lines;
|
|
266
|
+
return [...lines, "```"];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizeAssistantMarkdown(text: string): string {
|
|
270
|
+
const source = text.replace(/\r\n/g, "\n");
|
|
271
|
+
const hasHeadings = /^\s*#{1,6}\s+/m.test(source);
|
|
272
|
+
const sanitized = source.split("\n").map(sanitizeLine).map(normalizeListLine);
|
|
273
|
+
const titled = applyHeadingHeuristics(sanitized, hasHeadings);
|
|
274
|
+
const merged = mergeWrappedListItems(titled);
|
|
275
|
+
const fenced = fenceIndentedOutputBlocks(merged);
|
|
276
|
+
return ensureBalancedCodeFences(fenced).join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getMarkdownRenderer(useColor: boolean): MarkdownIt {
|
|
280
|
+
const key = useColor ? "color" : "plain";
|
|
281
|
+
const cached = markdownRenderers.get(key);
|
|
282
|
+
if (cached) return cached;
|
|
283
|
+
|
|
284
|
+
const renderer = new MarkdownIt({ html: false, linkify: true, typographer: true, breaks: false });
|
|
285
|
+
renderer.use(markdownItTerminal, {
|
|
286
|
+
indent: "",
|
|
287
|
+
...(useColor
|
|
288
|
+
? {}
|
|
289
|
+
: {
|
|
290
|
+
styleOptions: {
|
|
291
|
+
code: (s: string) => s,
|
|
292
|
+
blockquote: (s: string) => s,
|
|
293
|
+
html: (s: string) => s,
|
|
294
|
+
heading: (s: string) => s,
|
|
295
|
+
firstHeading: (s: string) => s,
|
|
296
|
+
hr: (s: string) => s,
|
|
297
|
+
listitem: (s: string) => s,
|
|
298
|
+
table: (s: string) => s,
|
|
299
|
+
paragraph: (s: string) => s,
|
|
300
|
+
strong: (s: string) => s,
|
|
301
|
+
em: (s: string) => s,
|
|
302
|
+
codespan: (s: string) => s,
|
|
303
|
+
del: (s: string) => s,
|
|
304
|
+
link: (s: string) => s,
|
|
305
|
+
href: (s: string) => s,
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
markdownRenderers.set(key, renderer);
|
|
311
|
+
return renderer;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function renderForTerminal(text: string, options: RenderOptions): string {
|
|
315
|
+
if (!options.renderMarkdown) return text;
|
|
316
|
+
const normalized = normalizeAssistantMarkdown(text);
|
|
317
|
+
try {
|
|
318
|
+
return getMarkdownRenderer(options.useColor).render(normalized, {});
|
|
319
|
+
} catch {
|
|
320
|
+
return formatMarkdownForTerminal(normalized, { useColor: options.useColor });
|
|
321
|
+
}
|
|
322
|
+
}
|