itermbot 1.0.2 → 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 +15 -20
- package/.github/workflows/release.yml +32 -20
- package/README.md +11 -20
- package/cleanup-unused.patch +108 -0
- package/config/app.yaml +32 -13
- package/config/memory.yaml +38 -31
- package/config/model.yaml +33 -0
- package/config/skill.yaml +8 -0
- package/config/tool.yaml +50 -17
- package/config/tsconfig.json +4 -1
- package/dist/chat/builtin-commands.d.ts +8 -0
- package/dist/chat/builtin-commands.d.ts.map +1 -0
- package/dist/chat/builtin-commands.js +53 -0
- package/dist/chat/builtin-commands.js.map +1 -0
- package/dist/chat/progress.d.ts +3 -0
- package/dist/chat/progress.d.ts.map +1 -0
- package/dist/chat/progress.js +23 -0
- package/dist/chat/progress.js.map +1 -0
- package/dist/chat/response-safety.d.ts +8 -0
- package/dist/chat/response-safety.d.ts.map +1 -0
- package/dist/chat/response-safety.js +126 -0
- package/dist/chat/response-safety.js.map +1 -0
- package/dist/chat/step-display.d.ts +2 -0
- package/dist/chat/step-display.d.ts.map +1 -0
- package/dist/chat/step-display.js +50 -0
- package/dist/chat/step-display.js.map +1 -0
- package/dist/chat/tool-result.d.ts +4 -0
- package/dist/chat/tool-result.d.ts.map +1 -0
- package/dist/chat/tool-result.js +24 -0
- package/dist/chat/tool-result.js.map +1 -0
- package/dist/config.d.ts +11 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -12
- package/dist/config.js.map +1 -1
- package/dist/index.js +308 -151
- package/dist/index.js.map +1 -1
- package/dist/iterm/direct-command-router.d.ts +24 -0
- package/dist/iterm/direct-command-router.d.ts.map +1 -0
- package/dist/iterm/direct-command-router.js +213 -0
- package/dist/iterm/direct-command-router.js.map +1 -0
- package/dist/iterm/session-hint.d.ts +10 -0
- package/dist/iterm/session-hint.d.ts.map +1 -0
- package/dist/iterm/session-hint.js +43 -0
- package/dist/iterm/session-hint.js.map +1 -0
- package/dist/iterm/target-panel-policy.d.ts +12 -0
- package/dist/iterm/target-panel-policy.d.ts.map +1 -0
- package/dist/iterm/target-panel-policy.js +287 -0
- package/dist/iterm/target-panel-policy.js.map +1 -0
- package/dist/runtime/text-tool-call-recovery.d.ts +23 -0
- package/dist/runtime/text-tool-call-recovery.d.ts.map +1 -0
- package/dist/runtime/text-tool-call-recovery.js +211 -0
- package/dist/runtime/text-tool-call-recovery.js.map +1 -0
- package/dist/startup/colors.d.ts +37 -0
- package/dist/startup/colors.d.ts.map +1 -0
- package/dist/{startup-colors.js → startup/colors.js} +30 -15
- package/dist/startup/colors.js.map +1 -0
- package/dist/startup/diagnostics.d.ts +8 -0
- package/dist/startup/diagnostics.d.ts.map +1 -0
- package/dist/startup/diagnostics.js +18 -0
- package/dist/startup/diagnostics.js.map +1 -0
- package/dist/startup/os.d.ts +10 -0
- package/dist/startup/os.d.ts.map +1 -0
- package/dist/startup/os.js +67 -0
- package/dist/startup/os.js.map +1 -0
- package/dist/startup/ui.d.ts +11 -0
- package/dist/startup/ui.d.ts.map +1 -0
- package/dist/startup/ui.js +49 -0
- package/dist/startup/ui.js.map +1 -0
- package/package.json +23 -13
- package/scripts/internal-package-refs.mjs +158 -0
- package/scripts/patch-buildin-cache.sh +1 -4
- package/scripts/resolve-deps.js +5 -0
- package/scripts/test-llm.mjs +11 -5
- package/skills/gpu-ssh-monitor/SKILL.md +22 -3
- package/src/chat/builtin-commands.ts +70 -0
- package/src/chat/progress.ts +26 -0
- package/src/chat/response-safety.ts +134 -0
- package/src/chat/step-display.ts +54 -0
- package/src/chat/tool-result.ts +22 -0
- package/src/config.ts +48 -21
- package/src/index.ts +377 -167
- package/src/iterm/direct-command-router.ts +274 -0
- package/src/iterm/session-hint.ts +49 -0
- package/src/iterm/target-panel-policy.ts +341 -0
- package/src/runtime/text-tool-call-recovery.ts +257 -0
- package/src/{startup-colors.ts → startup/colors.ts} +42 -27
- package/src/startup/diagnostics.ts +25 -0
- package/src/startup/os.ts +63 -0
- package/src/startup/ui.ts +56 -0
- package/src/types/marked-terminal.d.ts +3 -0
- package/test/builtin-commands.test.mjs +50 -0
- package/test/chat-flow.integration.test.mjs +235 -0
- package/test/chat-progress.test.mjs +83 -0
- package/test/config.test.mjs +22 -0
- package/test/diagnostics.test.mjs +45 -0
- package/test/direct-command-router.test.mjs +149 -0
- package/test/live-iterm-llm.integration.test.mjs +153 -0
- package/test/response-safety.test.mjs +44 -0
- package/test/session-hint.test.mjs +78 -0
- package/test/startup-colors.test.mjs +145 -0
- package/test/target-panel-policy.test.mjs +180 -0
- package/test/tool-call-recovery.test.mjs +199 -0
- package/config/agent.yaml +0 -121
- package/config/models.yaml +0 -36
- package/config/skills.yaml +0 -4
- package/dist/agent.d.ts +0 -14
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js +0 -16
- package/dist/agent.js.map +0 -1
- package/dist/context.d.ts +0 -12
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -20
- package/dist/context.js.map +0 -1
- package/dist/session-hint.d.ts +0 -4
- package/dist/session-hint.d.ts.map +0 -1
- package/dist/session-hint.js +0 -25
- package/dist/session-hint.js.map +0 -1
- package/dist/startup-colors.d.ts +0 -26
- package/dist/startup-colors.d.ts.map +0 -1
- package/dist/startup-colors.js.map +0 -1
- package/dist/target-routing.d.ts +0 -15
- package/dist/target-routing.d.ts.map +0 -1
- package/dist/target-routing.js +0 -355
- package/dist/target-routing.js.map +0 -1
- package/src/agent.ts +0 -35
- package/src/context.ts +0 -35
- package/src/session-hint.ts +0 -28
- package/src/target-routing.ts +0 -419
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asRecord,
|
|
3
|
+
normalizeToolList,
|
|
4
|
+
shortToolName,
|
|
5
|
+
} from "@easynet/agent-common/utils";
|
|
6
|
+
import { AgentContextTokens } from "@easynet/agent-common/context";
|
|
7
|
+
|
|
8
|
+
function normalizeToolInvokeResult(raw: unknown): unknown {
|
|
9
|
+
return raw;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function stringifyToolResult(value: unknown): string {
|
|
13
|
+
if (typeof value === "string") return value;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.stringify(value, null, 2);
|
|
16
|
+
} catch {
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type RuntimeLike = {
|
|
22
|
+
context: {
|
|
23
|
+
get<T>(token: unknown): T;
|
|
24
|
+
};
|
|
25
|
+
run: (input: string) => Promise<{ text?: string; messages?: unknown }>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface TextToolCallTargetHint {
|
|
29
|
+
windowId?: number;
|
|
30
|
+
tabIndex?: number;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ToolLike = {
|
|
35
|
+
name: string;
|
|
36
|
+
invoke: (args: unknown) => Promise<unknown>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface ToolCallPayload {
|
|
40
|
+
name: string;
|
|
41
|
+
arguments: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectToolObservationText(messages: unknown): string {
|
|
45
|
+
if (!Array.isArray(messages)) return "";
|
|
46
|
+
const chunks: string[] = [];
|
|
47
|
+
for (const message of messages) {
|
|
48
|
+
const rec = asRecord(message);
|
|
49
|
+
if (!rec) continue;
|
|
50
|
+
const type = typeof rec.type === "string" ? rec.type.toLowerCase() : "";
|
|
51
|
+
const role = typeof rec.role === "string" ? rec.role.toLowerCase() : "";
|
|
52
|
+
const isToolMessage = type.includes("tool") || role.includes("tool") || typeof rec.tool_call_id === "string";
|
|
53
|
+
if (!isToolMessage) continue;
|
|
54
|
+
const content = rec.content;
|
|
55
|
+
if (typeof content === "string" && content.trim().length > 0) {
|
|
56
|
+
chunks.push(content.trim());
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(content)) {
|
|
60
|
+
const joined = content
|
|
61
|
+
.map((part) => {
|
|
62
|
+
if (typeof part === "string") return part;
|
|
63
|
+
const obj = asRecord(part);
|
|
64
|
+
return typeof obj?.text === "string" ? obj.text : "";
|
|
65
|
+
})
|
|
66
|
+
.join(" ")
|
|
67
|
+
.trim();
|
|
68
|
+
if (joined) chunks.push(joined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return chunks.join("\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isToolLike(tool: unknown): tool is ToolLike {
|
|
75
|
+
return Boolean(
|
|
76
|
+
tool
|
|
77
|
+
&& typeof tool === "object"
|
|
78
|
+
&& typeof (tool as { name?: unknown }).name === "string"
|
|
79
|
+
&& typeof (tool as { invoke?: unknown }).invoke === "function",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractToolCallPayload(text: string): ToolCallPayload | null {
|
|
84
|
+
const match = text.match(/<tool-call>\s*([\s\S]*?)\s*<\/tool-call>/i);
|
|
85
|
+
if (match?.[1]) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(match[1]) as { name?: unknown; arguments?: unknown };
|
|
88
|
+
const name = typeof parsed.name === "string" ? parsed.name.trim() : "";
|
|
89
|
+
const args = asRecord(parsed.arguments) ?? {};
|
|
90
|
+
if (!name) return null;
|
|
91
|
+
return { name, arguments: args };
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fnMatch = text.match(/<function=([^\s>]+)>([\s\S]*)/i);
|
|
98
|
+
if (!fnMatch?.[1]) return null;
|
|
99
|
+
const name = fnMatch[1].trim();
|
|
100
|
+
if (!name) return null;
|
|
101
|
+
const tail = fnMatch[2] ?? "";
|
|
102
|
+
const args: Record<string, unknown> = {};
|
|
103
|
+
const paramRe = /<parameter=([^>]+)>\s*([\s\S]*?)(?=<parameter=[^>]+>|$)/gi;
|
|
104
|
+
for (const m of tail.matchAll(paramRe)) {
|
|
105
|
+
const key = (m[1] ?? "").trim();
|
|
106
|
+
const raw = (m[2] ?? "").trim();
|
|
107
|
+
if (!key || !raw) continue;
|
|
108
|
+
if (/^-?\d+$/.test(raw)) {
|
|
109
|
+
args[key] = Number(raw);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
args[key] = raw;
|
|
113
|
+
}
|
|
114
|
+
return { name, arguments: args };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function findToolByName(runtime: RuntimeLike, requestedName: string): ToolLike | null {
|
|
118
|
+
const rawTools = runtime.context.get<unknown>(AgentContextTokens.Tools);
|
|
119
|
+
const tools = normalizeToolList<ToolLike>(rawTools, isToolLike);
|
|
120
|
+
if (tools.length === 0) return null;
|
|
121
|
+
const exact = tools.find((tool) => shortToolName(tool.name) === requestedName);
|
|
122
|
+
if (exact) return exact;
|
|
123
|
+
const contains = tools.find((tool) => tool.name.includes(requestedName));
|
|
124
|
+
return contains ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildRecoveredEvidence(raw: unknown): string {
|
|
128
|
+
const root = asRecord(raw);
|
|
129
|
+
const result = asRecord(root?.result);
|
|
130
|
+
if (!result) return "";
|
|
131
|
+
|
|
132
|
+
const blockedToolRaw = typeof result.blockedTool === "string" ? result.blockedTool : "";
|
|
133
|
+
const blockedTool = blockedToolRaw.split(".").pop() ?? blockedToolRaw;
|
|
134
|
+
const requestedPath = typeof result.requestedPath === "string" ? result.requestedPath : "";
|
|
135
|
+
const resolvedPath = typeof result.resolvedPath === "string" ? result.resolvedPath : "";
|
|
136
|
+
const pathFallbackUsed = typeof result.pathFallbackUsed === "boolean" ? result.pathFallbackUsed : false;
|
|
137
|
+
|
|
138
|
+
const outputRoot = asRecord(result.output);
|
|
139
|
+
const outputResult = asRecord(outputRoot?.result);
|
|
140
|
+
const outputText = typeof outputResult?.output === "string" ? outputResult.output : "";
|
|
141
|
+
|
|
142
|
+
if (blockedTool !== "listDir") return "";
|
|
143
|
+
|
|
144
|
+
const listingLines = outputText
|
|
145
|
+
.split("\n")
|
|
146
|
+
.map((line) => line.trim())
|
|
147
|
+
.filter((line) => line.length > 0 && (line.startsWith("./") || line.startsWith("../") || line.startsWith("/")))
|
|
148
|
+
.slice(0, 120);
|
|
149
|
+
|
|
150
|
+
const parts = [
|
|
151
|
+
"Structured execution facts:",
|
|
152
|
+
`- blockedTool: ${blockedTool}`,
|
|
153
|
+
`- requestedPath: ${requestedPath || "(unknown)"}`,
|
|
154
|
+
`- resolvedPath: ${resolvedPath || requestedPath || "(unknown)"}`,
|
|
155
|
+
`- pathFallbackUsed: ${String(pathFallbackUsed)}`,
|
|
156
|
+
];
|
|
157
|
+
if (listingLines.length > 0) {
|
|
158
|
+
parts.push("- extractedListing:");
|
|
159
|
+
parts.push(listingLines.join("\n"));
|
|
160
|
+
}
|
|
161
|
+
return parts.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatStepNumber(stepNumber: number): string {
|
|
165
|
+
return String(Math.max(0, stepNumber)).padStart(2, "0");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeStepRunStart(writer: (line: string) => void, label: string): number {
|
|
169
|
+
writer("");
|
|
170
|
+
writer(`=== Steps: ${label} ===`);
|
|
171
|
+
return Date.now();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function writeStepStart(writer: (line: string) => void, stepNumber: number, action: string): void {
|
|
175
|
+
writer(`[${formatStepNumber(stepNumber)}] ▶ ${action}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function writeStepDone(writer: (line: string) => void, stepNumber: number, action: string): void {
|
|
179
|
+
writer(`[${formatStepNumber(stepNumber)}] ✓ ${action}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function writeStepProgress(writer: (line: string) => void, completed: number, total: number): void {
|
|
183
|
+
writer(` progress ${completed}/${total}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function writeStepRunDone(writer: (line: string) => void, startedAtMs: number, steps: number): void {
|
|
187
|
+
const elapsed = Math.max(0, Math.round(Date.now() - startedAtMs));
|
|
188
|
+
writer(`=== Steps complete: ${steps} step(s), ${elapsed}ms ===`);
|
|
189
|
+
writer("");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatRecoveredToolResult(payloadName: string, raw: unknown): string {
|
|
193
|
+
const recoveredEvidence = buildRecoveredEvidence(raw);
|
|
194
|
+
if (recoveredEvidence) {
|
|
195
|
+
return [
|
|
196
|
+
"### Recovered Tool Result",
|
|
197
|
+
`- Tool: \`${payloadName}\``,
|
|
198
|
+
"",
|
|
199
|
+
recoveredEvidence,
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
202
|
+
return stringifyToolResult(raw);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function mergeTargetHintForItermTool(
|
|
206
|
+
toolName: string,
|
|
207
|
+
args: Record<string, unknown>,
|
|
208
|
+
hint?: TextToolCallTargetHint,
|
|
209
|
+
): Record<string, unknown> {
|
|
210
|
+
if (!toolName.includes("itermRunCommandInSession")) return args;
|
|
211
|
+
return {
|
|
212
|
+
...args,
|
|
213
|
+
...(args.windowId == null && hint?.windowId != null ? { windowId: hint.windowId } : {}),
|
|
214
|
+
...(args.tabIndex == null && hint?.tabIndex != null ? { tabIndex: hint.tabIndex } : {}),
|
|
215
|
+
...(args.sessionId == null && hint?.sessionId ? { sessionId: hint.sessionId } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function runWithTextToolCallRecovery(
|
|
220
|
+
runtime: RuntimeLike,
|
|
221
|
+
userInput: string,
|
|
222
|
+
writer: (line: string) => void = console.log,
|
|
223
|
+
targetHint?: TextToolCallTargetHint,
|
|
224
|
+
): Promise<{ text: string; recovered: boolean; evidenceText?: string }> {
|
|
225
|
+
const first = await runtime.run(userInput);
|
|
226
|
+
const firstText = (first.text ?? "").trim();
|
|
227
|
+
const payload = extractToolCallPayload(firstText);
|
|
228
|
+
if (!payload) {
|
|
229
|
+
return { text: firstText, recovered: false, evidenceText: collectToolObservationText(first.messages) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tool = findToolByName(runtime, payload.name);
|
|
233
|
+
if (!tool) return { text: firstText, recovered: false, evidenceText: collectToolObservationText(first.messages) };
|
|
234
|
+
|
|
235
|
+
const recoveryStartedAtMs = writeStepRunStart(writer, "recover text tool-call");
|
|
236
|
+
writeStepStart(writer, 1, `execute recovered tool call: ${payload.name}`);
|
|
237
|
+
const mergedArgs = mergeTargetHintForItermTool(tool.name, payload.arguments, targetHint);
|
|
238
|
+
if (tool.name.includes("itermRunCommandInSession")) {
|
|
239
|
+
const cmd = typeof mergedArgs.command === "string" ? mergedArgs.command.trim() : "";
|
|
240
|
+
if (cmd) writer(" reason: Because the previous step produced a literal tool-call, execute it first to recover deterministic evidence.");
|
|
241
|
+
}
|
|
242
|
+
const raw = normalizeToolInvokeResult(await tool.invoke(mergedArgs));
|
|
243
|
+
writeStepDone(writer, 1, `execute recovered tool call: ${payload.name}`);
|
|
244
|
+
writeStepProgress(writer, 1, 2);
|
|
245
|
+
|
|
246
|
+
writeStepStart(writer, 2, "finalize response from recovered tool result");
|
|
247
|
+
const toolResult = stringifyToolResult(raw);
|
|
248
|
+
const secondText = formatRecoveredToolResult(payload.name, raw);
|
|
249
|
+
writeStepDone(writer, 2, "finalize response from recovered tool result");
|
|
250
|
+
writeStepProgress(writer, 2, 2);
|
|
251
|
+
writeStepRunDone(writer, recoveryStartedAtMs, 2);
|
|
252
|
+
|
|
253
|
+
return { text: secondText || toolResult, recovered: true, evidenceText: toolResult };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export type RecoveryTargetHint = TextToolCallTargetHint;
|
|
257
|
+
export const runWithToolCallRecovery = runWithTextToolCallRecovery;
|
|
@@ -111,14 +111,6 @@ function captureSessionColorsSync(args: {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export function captureSessionColorsByIdSync(args: {
|
|
115
|
-
windowId: number;
|
|
116
|
-
tabIndex: number;
|
|
117
|
-
sessionId: string;
|
|
118
|
-
}): SessionColorSnapshot | null {
|
|
119
|
-
return captureSessionColorsSync(args);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
114
|
export function restoreSessionColorsSync(snapshots: SessionColorSnapshot[]): void {
|
|
123
115
|
for (const s of snapshots) {
|
|
124
116
|
try {
|
|
@@ -180,7 +172,34 @@ export interface StartupResult {
|
|
|
180
172
|
colorSnapshots: SessionColorSnapshot[];
|
|
181
173
|
}
|
|
182
174
|
|
|
183
|
-
|
|
175
|
+
type ListSessionsResult = Awaited<ReturnType<typeof itermListCurrentWindowSessions>>;
|
|
176
|
+
|
|
177
|
+
export interface StartupPanelColorDeps {
|
|
178
|
+
getProcessTty: () => string | null;
|
|
179
|
+
listCurrentWindowSessions: () => Promise<ListSessionsResult>;
|
|
180
|
+
splitPane: typeof itermSplitPane;
|
|
181
|
+
setSessionColors: typeof itermSetSessionColors;
|
|
182
|
+
captureSessionColors: (args: { windowId: number; tabIndex: number; sessionId: string }) => SessionColorSnapshot | null;
|
|
183
|
+
onError: (message: string, error?: unknown) => void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createDefaultDeps(): StartupPanelColorDeps {
|
|
187
|
+
return {
|
|
188
|
+
getProcessTty: findProcessTty,
|
|
189
|
+
listCurrentWindowSessions: itermListCurrentWindowSessions,
|
|
190
|
+
splitPane: itermSplitPane,
|
|
191
|
+
setSessionColors: itermSetSessionColors,
|
|
192
|
+
captureSessionColors: captureSessionColorsSync,
|
|
193
|
+
onError: (message: string, error?: unknown) => {
|
|
194
|
+
if (error === undefined) console.error(message);
|
|
195
|
+
else console.error(message, error instanceof Error ? error.message : error);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function applyStartupPanelColorsWithDeps(
|
|
201
|
+
deps: StartupPanelColorDeps,
|
|
202
|
+
): Promise<StartupResult> {
|
|
184
203
|
const empty: StartupResult = {
|
|
185
204
|
chatSessionId: null,
|
|
186
205
|
targetSessionId: null,
|
|
@@ -189,8 +208,8 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
189
208
|
colorSnapshots: [],
|
|
190
209
|
};
|
|
191
210
|
|
|
192
|
-
const myTty =
|
|
193
|
-
const { result } = await
|
|
211
|
+
const myTty = deps.getProcessTty();
|
|
212
|
+
const { result } = await deps.listCurrentWindowSessions();
|
|
194
213
|
|
|
195
214
|
if (result.count === 0) return empty;
|
|
196
215
|
|
|
@@ -201,9 +220,7 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
201
220
|
: sessions.find((s) => s.isCurrentSession);
|
|
202
221
|
|
|
203
222
|
if (!chatSession) {
|
|
204
|
-
|
|
205
|
-
`startup-colors: could not identify chat session (tty=${myTty}), skipping`,
|
|
206
|
-
);
|
|
223
|
+
deps.onError(`startup-colors: could not identify chat session (tty=${myTty}), skipping`);
|
|
207
224
|
return empty;
|
|
208
225
|
}
|
|
209
226
|
|
|
@@ -221,7 +238,7 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
221
238
|
// If only chat panel exists, split once to create target panel on startup.
|
|
222
239
|
if (sameTabSessions.length === 1 && chatSessionId) {
|
|
223
240
|
try {
|
|
224
|
-
await
|
|
241
|
+
await deps.splitPane({
|
|
225
242
|
windowId,
|
|
226
243
|
tabIndex: currentTabIndex,
|
|
227
244
|
sessionId: chatSessionId,
|
|
@@ -229,7 +246,7 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
229
246
|
activate: false,
|
|
230
247
|
});
|
|
231
248
|
|
|
232
|
-
const refreshed = await
|
|
249
|
+
const refreshed = await deps.listCurrentWindowSessions();
|
|
233
250
|
sessions = refreshed.result.sessions as SessionInfo[];
|
|
234
251
|
const preservedChat = sessions.find((s) => s.sessionId === chatSessionId);
|
|
235
252
|
if (preservedChat) {
|
|
@@ -240,10 +257,7 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
240
257
|
sameTabSessions = sessions.filter((s) => s.tabIndex === currentTabIndex);
|
|
241
258
|
targetSessions = sameTabSessions.filter((s) => s.sessionId !== chatSessionId);
|
|
242
259
|
} catch (err) {
|
|
243
|
-
|
|
244
|
-
"startup-colors: failed to auto-split single panel:",
|
|
245
|
-
err instanceof Error ? err.message : err,
|
|
246
|
-
);
|
|
260
|
+
deps.onError("startup-colors: failed to auto-split single panel:", err);
|
|
247
261
|
}
|
|
248
262
|
}
|
|
249
263
|
|
|
@@ -266,7 +280,7 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
266
280
|
for (const s of sessionsToSnapshot) {
|
|
267
281
|
if (!s.sessionId || seen.has(s.sessionId)) continue;
|
|
268
282
|
seen.add(s.sessionId);
|
|
269
|
-
const snapshot =
|
|
283
|
+
const snapshot = deps.captureSessionColors({
|
|
270
284
|
windowId,
|
|
271
285
|
tabIndex: currentTabIndex,
|
|
272
286
|
sessionId: s.sessionId,
|
|
@@ -280,7 +294,7 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
280
294
|
|
|
281
295
|
if (chatSession.sessionId) {
|
|
282
296
|
tasks.push(
|
|
283
|
-
|
|
297
|
+
deps.setSessionColors({
|
|
284
298
|
windowId,
|
|
285
299
|
tabIndex: currentTabIndex,
|
|
286
300
|
sessionId: chatSession.sessionId,
|
|
@@ -292,7 +306,7 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
292
306
|
|
|
293
307
|
if (selectedTarget?.sessionId) {
|
|
294
308
|
tasks.push(
|
|
295
|
-
|
|
309
|
+
deps.setSessionColors({
|
|
296
310
|
windowId,
|
|
297
311
|
tabIndex: currentTabIndex,
|
|
298
312
|
sessionId: selectedTarget.sessionId,
|
|
@@ -306,12 +320,13 @@ export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
|
306
320
|
|
|
307
321
|
for (const r of results) {
|
|
308
322
|
if (r.status === "rejected") {
|
|
309
|
-
|
|
310
|
-
"startup-colors: failed to set color on a session:",
|
|
311
|
-
r.reason?.message ?? r.reason,
|
|
312
|
-
);
|
|
323
|
+
deps.onError("startup-colors: failed to set color on a session:", r.reason);
|
|
313
324
|
}
|
|
314
325
|
}
|
|
315
326
|
|
|
316
327
|
return startupResult;
|
|
317
328
|
}
|
|
329
|
+
|
|
330
|
+
export async function applyStartupPanelColors(): Promise<StartupResult> {
|
|
331
|
+
return applyStartupPanelColorsWithDeps(createDefaultDeps());
|
|
332
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AgentContextTokens } from "@easynet/agent-common/context";
|
|
2
|
+
import { extractTextFromLlmOutput } from "@easynet/agent-common/utils";
|
|
3
|
+
|
|
4
|
+
const LLM_HEALTHCHECK_PROMPT = "Health check: reply with OK only.";
|
|
5
|
+
|
|
6
|
+
type AgentRuntimeLike = {
|
|
7
|
+
context: {
|
|
8
|
+
get<T>(token: unknown): T;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function runLlmHealthCheck(runtime: AgentRuntimeLike): Promise<string> {
|
|
13
|
+
const llm = runtime.context.get<{ invoke: (input: string) => Promise<unknown> }>(AgentContextTokens.ChatModel);
|
|
14
|
+
const timeoutMs = Number(process.env.ITERMBOT_LLM_HEALTHCHECK_TIMEOUT_MS ?? "12000");
|
|
15
|
+
const timeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 12000;
|
|
16
|
+
|
|
17
|
+
const invokePromise = llm.invoke(LLM_HEALTHCHECK_PROMPT);
|
|
18
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
19
|
+
setTimeout(() => reject(new Error(`LLM health check timeout (${timeout}ms)`)), timeout);
|
|
20
|
+
});
|
|
21
|
+
const output = await Promise.race([invokePromise, timeoutPromise]);
|
|
22
|
+
const text = extractTextFromLlmOutput(output);
|
|
23
|
+
if (!text) throw new Error("LLM health check returned empty response");
|
|
24
|
+
return text.length > 32 ? `${text.slice(0, 32)}...` : text;
|
|
25
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { itermRunCommandInSession } from "@easynet/agent-tool-buildin/iterm";
|
|
2
|
+
|
|
3
|
+
export interface StartupOsInfo {
|
|
4
|
+
os: string;
|
|
5
|
+
source: "target-panel" | "local-fallback";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeOsName(raw: string): string {
|
|
9
|
+
const normalized = raw.trim();
|
|
10
|
+
if (!normalized) return "unknown";
|
|
11
|
+
if (/^darwin$/i.test(normalized)) return "darwin";
|
|
12
|
+
if (/^linux$/i.test(normalized)) return "linux";
|
|
13
|
+
if (/^(msys|mingw|cygwin|windows_nt)/i.test(normalized)) return "windows";
|
|
14
|
+
if (/^freebsd$/i.test(normalized)) return "freebsd";
|
|
15
|
+
if (/^openbsd$/i.test(normalized)) return "openbsd";
|
|
16
|
+
if (/^netbsd$/i.test(normalized)) return "netbsd";
|
|
17
|
+
if (/^sunos$/i.test(normalized)) return "sunos";
|
|
18
|
+
if (/^aix$/i.test(normalized)) return "aix";
|
|
19
|
+
return normalized.toLowerCase();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function inferFromLocalProcessPlatform(): string {
|
|
23
|
+
const p = process.platform;
|
|
24
|
+
if (p === "darwin") return "darwin";
|
|
25
|
+
if (p === "linux") return "linux";
|
|
26
|
+
if (p === "win32") return "windows";
|
|
27
|
+
return p;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractOsFromCommandOutput(output: unknown): string {
|
|
31
|
+
if (typeof output !== "string" || !output.trim()) return "unknown";
|
|
32
|
+
const lines = output
|
|
33
|
+
.split("\n")
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter((line) => line.length > 0);
|
|
36
|
+
for (const line of lines) {
|
|
37
|
+
const match = line.match(/\b(Darwin|Linux|FreeBSD|OpenBSD|NetBSD|SunOS|AIX|MSYS|MINGW|CYGWIN|Windows_NT)\b/i);
|
|
38
|
+
if (match?.[1]) return normalizeOsName(match[1]);
|
|
39
|
+
}
|
|
40
|
+
return "unknown";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function detectStartupTargetOs(args: {
|
|
44
|
+
windowId?: number;
|
|
45
|
+
tabIndex?: number;
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
}): Promise<StartupOsInfo> {
|
|
48
|
+
try {
|
|
49
|
+
const out = await itermRunCommandInSession({
|
|
50
|
+
command: "uname -s",
|
|
51
|
+
waitMs: 400,
|
|
52
|
+
maxOutputLines: 120,
|
|
53
|
+
windowId: args.windowId,
|
|
54
|
+
tabIndex: args.tabIndex,
|
|
55
|
+
sessionId: args.sessionId,
|
|
56
|
+
});
|
|
57
|
+
const detected = extractOsFromCommandOutput(out?.result?.output);
|
|
58
|
+
if (detected !== "unknown") return { os: detected, source: "target-panel" };
|
|
59
|
+
} catch {
|
|
60
|
+
// fall back to local process platform
|
|
61
|
+
}
|
|
62
|
+
return { os: inferFromLocalProcessPlatform(), source: "local-fallback" };
|
|
63
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
formatLogPath,
|
|
6
|
+
runStartupStep,
|
|
7
|
+
StartupProgressRenderer,
|
|
8
|
+
toErrorMessage,
|
|
9
|
+
} from "@easynet/agent-common/cli";
|
|
10
|
+
|
|
11
|
+
export { formatLogPath, runStartupStep, StartupProgressRenderer, toErrorMessage };
|
|
12
|
+
|
|
13
|
+
export function clearStartupNoise(): void {
|
|
14
|
+
if (!process.stdout.isTTY) return;
|
|
15
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function printStartupBanner(): void {
|
|
19
|
+
const lines = [
|
|
20
|
+
" _ _____ ____ _ ",
|
|
21
|
+
" (_)_ _|__ _ __ _ __ ___ | __ ) ___ | |_ ",
|
|
22
|
+
" | | | |/ _ \\ '__| '_ ` _ \\| _ \\ / _ \\| __|",
|
|
23
|
+
" | | | | __/ | | | | | | | |_) | (_) | |_ ",
|
|
24
|
+
" |_| |_|\\___|_| |_| |_| |_|____/ \\___/ \\__|",
|
|
25
|
+
" iTermBot ",
|
|
26
|
+
];
|
|
27
|
+
console.log("--------------------------------------------------------------------------------------------------");
|
|
28
|
+
console.log(`\n${lines.join("\n")}`);
|
|
29
|
+
console.log(" Observe the terminal. Act with precision.\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getAppVersion(): string {
|
|
33
|
+
try {
|
|
34
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
35
|
+
const packageJsonPath = resolve(dirname(currentFile), "../package.json");
|
|
36
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: unknown };
|
|
37
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
38
|
+
} catch {
|
|
39
|
+
return "unknown";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function printStartupSummary(args: { version: string; responseSafetyMode?: string; targetOs?: string }): void {
|
|
44
|
+
console.log("\n--------------------------------------------------------------------------------------------------");
|
|
45
|
+
console.log(`Version : v${args.version}`);
|
|
46
|
+
console.log(`Workspace : ${process.cwd()}`);
|
|
47
|
+
if (args.targetOs) {
|
|
48
|
+
console.log(`Target OS : ${args.targetOs}`);
|
|
49
|
+
}
|
|
50
|
+
if (args.responseSafetyMode) {
|
|
51
|
+
console.log(`Safety : response mode=${args.responseSafetyMode}`);
|
|
52
|
+
}
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log("Commands : list tools, list skills, exit");
|
|
55
|
+
console.log("--------------------------------------------------------------------------------------------------\n");
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { AgentContextTokens } from "@easynet/agent-common/context";
|
|
4
|
+
import { tryHandleBuiltinReadCommand } from "../dist/chat/builtin-commands.js";
|
|
5
|
+
|
|
6
|
+
function makeRuntime({ tools = [], skills = [] } = {}) {
|
|
7
|
+
return {
|
|
8
|
+
context: {
|
|
9
|
+
get(token) {
|
|
10
|
+
if (token === AgentContextTokens.Tools) return tools;
|
|
11
|
+
if (token === AgentContextTokens.SkillSet) {
|
|
12
|
+
return { list: () => skills };
|
|
13
|
+
}
|
|
14
|
+
return undefined;
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("tryHandleBuiltinReadCommand: list tools returns local tool list", () => {
|
|
21
|
+
const runtime = makeRuntime({
|
|
22
|
+
tools: [
|
|
23
|
+
{ name: "npm.easynet.agent.tool.buildin.0.0.70.listDir" },
|
|
24
|
+
{ name: "npm.easynet.agent.tool.buildin.0.0.70.itermRunCommandInSession", description: "Run one command in target session." },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
const out = tryHandleBuiltinReadCommand("list tools", runtime);
|
|
28
|
+
assert.equal(out?.includes("Available tools (2):"), true);
|
|
29
|
+
assert.equal(out?.includes("- listDir"), true);
|
|
30
|
+
assert.equal(out?.includes("- itermRunCommandInSession: Run one command in target session."), true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("tryHandleBuiltinReadCommand: list skills returns local skill list", () => {
|
|
34
|
+
const runtime = makeRuntime({
|
|
35
|
+
skills: [
|
|
36
|
+
{ name: "skill-a", description: "Do A" },
|
|
37
|
+
{ name: "skill-b" },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
const out = tryHandleBuiltinReadCommand(" list skills ", runtime);
|
|
41
|
+
assert.equal(out?.includes("Available skills (2):"), true);
|
|
42
|
+
assert.equal(out?.includes("- skill-a: Do A"), true);
|
|
43
|
+
assert.equal(out?.includes("- skill-b"), true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("tryHandleBuiltinReadCommand: unknown command returns null", () => {
|
|
47
|
+
const runtime = makeRuntime();
|
|
48
|
+
const out = tryHandleBuiltinReadCommand("check disk usage", runtime);
|
|
49
|
+
assert.equal(out, null);
|
|
50
|
+
});
|