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.
Files changed (128) hide show
  1. package/.github/workflows/ci.yml +15 -20
  2. package/.github/workflows/release.yml +32 -20
  3. package/README.md +11 -20
  4. package/cleanup-unused.patch +108 -0
  5. package/config/app.yaml +32 -13
  6. package/config/memory.yaml +38 -31
  7. package/config/model.yaml +33 -0
  8. package/config/skill.yaml +8 -0
  9. package/config/tool.yaml +50 -17
  10. package/config/tsconfig.json +4 -1
  11. package/dist/chat/builtin-commands.d.ts +8 -0
  12. package/dist/chat/builtin-commands.d.ts.map +1 -0
  13. package/dist/chat/builtin-commands.js +53 -0
  14. package/dist/chat/builtin-commands.js.map +1 -0
  15. package/dist/chat/progress.d.ts +3 -0
  16. package/dist/chat/progress.d.ts.map +1 -0
  17. package/dist/chat/progress.js +23 -0
  18. package/dist/chat/progress.js.map +1 -0
  19. package/dist/chat/response-safety.d.ts +8 -0
  20. package/dist/chat/response-safety.d.ts.map +1 -0
  21. package/dist/chat/response-safety.js +126 -0
  22. package/dist/chat/response-safety.js.map +1 -0
  23. package/dist/chat/step-display.d.ts +2 -0
  24. package/dist/chat/step-display.d.ts.map +1 -0
  25. package/dist/chat/step-display.js +50 -0
  26. package/dist/chat/step-display.js.map +1 -0
  27. package/dist/chat/tool-result.d.ts +4 -0
  28. package/dist/chat/tool-result.d.ts.map +1 -0
  29. package/dist/chat/tool-result.js +24 -0
  30. package/dist/chat/tool-result.js.map +1 -0
  31. package/dist/config.d.ts +11 -6
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +26 -12
  34. package/dist/config.js.map +1 -1
  35. package/dist/index.js +308 -151
  36. package/dist/index.js.map +1 -1
  37. package/dist/iterm/direct-command-router.d.ts +24 -0
  38. package/dist/iterm/direct-command-router.d.ts.map +1 -0
  39. package/dist/iterm/direct-command-router.js +213 -0
  40. package/dist/iterm/direct-command-router.js.map +1 -0
  41. package/dist/iterm/session-hint.d.ts +10 -0
  42. package/dist/iterm/session-hint.d.ts.map +1 -0
  43. package/dist/iterm/session-hint.js +43 -0
  44. package/dist/iterm/session-hint.js.map +1 -0
  45. package/dist/iterm/target-panel-policy.d.ts +12 -0
  46. package/dist/iterm/target-panel-policy.d.ts.map +1 -0
  47. package/dist/iterm/target-panel-policy.js +287 -0
  48. package/dist/iterm/target-panel-policy.js.map +1 -0
  49. package/dist/runtime/text-tool-call-recovery.d.ts +23 -0
  50. package/dist/runtime/text-tool-call-recovery.d.ts.map +1 -0
  51. package/dist/runtime/text-tool-call-recovery.js +211 -0
  52. package/dist/runtime/text-tool-call-recovery.js.map +1 -0
  53. package/dist/startup/colors.d.ts +37 -0
  54. package/dist/startup/colors.d.ts.map +1 -0
  55. package/dist/{startup-colors.js → startup/colors.js} +30 -15
  56. package/dist/startup/colors.js.map +1 -0
  57. package/dist/startup/diagnostics.d.ts +8 -0
  58. package/dist/startup/diagnostics.d.ts.map +1 -0
  59. package/dist/startup/diagnostics.js +18 -0
  60. package/dist/startup/diagnostics.js.map +1 -0
  61. package/dist/startup/os.d.ts +10 -0
  62. package/dist/startup/os.d.ts.map +1 -0
  63. package/dist/startup/os.js +67 -0
  64. package/dist/startup/os.js.map +1 -0
  65. package/dist/startup/ui.d.ts +11 -0
  66. package/dist/startup/ui.d.ts.map +1 -0
  67. package/dist/startup/ui.js +49 -0
  68. package/dist/startup/ui.js.map +1 -0
  69. package/package.json +23 -13
  70. package/scripts/internal-package-refs.mjs +158 -0
  71. package/scripts/patch-buildin-cache.sh +1 -4
  72. package/scripts/resolve-deps.js +5 -0
  73. package/scripts/test-llm.mjs +11 -5
  74. package/skills/gpu-ssh-monitor/SKILL.md +22 -3
  75. package/src/chat/builtin-commands.ts +70 -0
  76. package/src/chat/progress.ts +26 -0
  77. package/src/chat/response-safety.ts +134 -0
  78. package/src/chat/step-display.ts +54 -0
  79. package/src/chat/tool-result.ts +22 -0
  80. package/src/config.ts +48 -21
  81. package/src/index.ts +377 -167
  82. package/src/iterm/direct-command-router.ts +274 -0
  83. package/src/iterm/session-hint.ts +49 -0
  84. package/src/iterm/target-panel-policy.ts +341 -0
  85. package/src/runtime/text-tool-call-recovery.ts +257 -0
  86. package/src/{startup-colors.ts → startup/colors.ts} +42 -27
  87. package/src/startup/diagnostics.ts +25 -0
  88. package/src/startup/os.ts +63 -0
  89. package/src/startup/ui.ts +56 -0
  90. package/src/types/marked-terminal.d.ts +3 -0
  91. package/test/builtin-commands.test.mjs +50 -0
  92. package/test/chat-flow.integration.test.mjs +235 -0
  93. package/test/chat-progress.test.mjs +83 -0
  94. package/test/config.test.mjs +22 -0
  95. package/test/diagnostics.test.mjs +45 -0
  96. package/test/direct-command-router.test.mjs +149 -0
  97. package/test/live-iterm-llm.integration.test.mjs +153 -0
  98. package/test/response-safety.test.mjs +44 -0
  99. package/test/session-hint.test.mjs +78 -0
  100. package/test/startup-colors.test.mjs +145 -0
  101. package/test/target-panel-policy.test.mjs +180 -0
  102. package/test/tool-call-recovery.test.mjs +199 -0
  103. package/config/agent.yaml +0 -121
  104. package/config/models.yaml +0 -36
  105. package/config/skills.yaml +0 -4
  106. package/dist/agent.d.ts +0 -14
  107. package/dist/agent.d.ts.map +0 -1
  108. package/dist/agent.js +0 -16
  109. package/dist/agent.js.map +0 -1
  110. package/dist/context.d.ts +0 -12
  111. package/dist/context.d.ts.map +0 -1
  112. package/dist/context.js +0 -20
  113. package/dist/context.js.map +0 -1
  114. package/dist/session-hint.d.ts +0 -4
  115. package/dist/session-hint.d.ts.map +0 -1
  116. package/dist/session-hint.js +0 -25
  117. package/dist/session-hint.js.map +0 -1
  118. package/dist/startup-colors.d.ts +0 -26
  119. package/dist/startup-colors.d.ts.map +0 -1
  120. package/dist/startup-colors.js.map +0 -1
  121. package/dist/target-routing.d.ts +0 -15
  122. package/dist/target-routing.d.ts.map +0 -1
  123. package/dist/target-routing.js +0 -355
  124. package/dist/target-routing.js.map +0 -1
  125. package/src/agent.ts +0 -35
  126. package/src/context.ts +0 -35
  127. package/src/session-hint.ts +0 -28
  128. 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
- export async function applyStartupPanelColors(): Promise<StartupResult> {
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 = findProcessTty();
193
- const { result } = await itermListCurrentWindowSessions();
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
- console.error(
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 itermSplitPane({
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 itermListCurrentWindowSessions();
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
- console.error(
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 = captureSessionColorsSync({
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
- itermSetSessionColors({
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
- itermSetSessionColors({
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
- console.error(
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,3 @@
1
+ declare module "marked-terminal" {
2
+ export function markedTerminal(options?: Record<string, unknown>): unknown;
3
+ }
@@ -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
+ });