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
package/src/config.ts CHANGED
@@ -1,26 +1,53 @@
1
- import {
2
- createRuntimeConfig,
3
- getModelsConfigPath as getRuntimeModelsConfigPath,
4
- getMemoryConfigPath as getRuntimeMemoryConfigPath,
5
- getToolConfigPath as getRuntimeToolConfigPath,
6
- type AgentRuntimeConfig,
7
- } from "@easynet/agent-runtime";
1
+ import { resolveKindResourceFile, asObject } from "@easynet/agent-common/config";
8
2
 
9
- export type AppConfig = AgentRuntimeConfig;
3
+ export type PromptTemplates = {
4
+ systemPrompt?: string;
5
+ };
10
6
 
11
- export async function loadAppConfig(configPath?: string): Promise<AppConfig> {
12
- const runtimeConfig = await createRuntimeConfig({ configPath });
13
- return runtimeConfig;
14
- }
15
-
16
- export function getModelsConfigPath(config: AppConfig, agentName?: string): string {
17
- return getRuntimeModelsConfigPath(config, agentName);
18
- }
7
+ export type ResponseSafetyMode = "off" | "balanced" | "strict";
19
8
 
20
- export function getMemoryConfigPath(config: AppConfig, agentName?: string): string {
21
- return getRuntimeMemoryConfigPath(config, agentName);
22
- }
9
+ export type AppConfig = {
10
+ printSteps?: boolean;
11
+ maxSteps?: number;
12
+ promptTemplates?: PromptTemplates;
13
+ responseSafetyMode?: ResponseSafetyMode;
14
+ };
23
15
 
24
- export function getToolConfigPath(config: AppConfig, agentName?: string): string {
25
- return getRuntimeToolConfigPath(config, agentName);
16
+ export async function loadAppConfig(configPath: string): Promise<AppConfig> {
17
+ try {
18
+ const resource = await resolveKindResourceFile<{
19
+ printSteps?: unknown;
20
+ maxSteps?: unknown;
21
+ promptTemplates?: PromptTemplates;
22
+ responseSafetyMode?: unknown;
23
+ }>(configPath, {
24
+ baseDir: process.cwd(),
25
+ expectedApiVersion: "easynet.world/v1",
26
+ expectedKind: "AppConfig",
27
+ });
28
+ const spec = asObject(resource.spec) as {
29
+ printSteps?: unknown;
30
+ maxSteps?: unknown;
31
+ promptTemplates?: PromptTemplates;
32
+ responseSafetyMode?: unknown;
33
+ } | undefined;
34
+ const modeRaw = typeof spec?.responseSafetyMode === "string" ? spec.responseSafetyMode.trim().toLowerCase() : "";
35
+ const responseSafetyMode =
36
+ modeRaw === "off" || modeRaw === "balanced" || modeRaw === "strict"
37
+ ? (modeRaw as ResponseSafetyMode)
38
+ : undefined;
39
+ const printSteps = typeof spec?.printSteps === "boolean" ? spec.printSteps : undefined;
40
+ const maxSteps =
41
+ typeof spec?.maxSteps === "number" && Number.isFinite(spec.maxSteps)
42
+ ? Math.max(1, Math.floor(spec.maxSteps))
43
+ : undefined;
44
+ return {
45
+ printSteps,
46
+ maxSteps,
47
+ promptTemplates: spec?.promptTemplates,
48
+ responseSafetyMode,
49
+ };
50
+ } catch {
51
+ return {};
52
+ }
26
53
  }
package/src/index.ts CHANGED
@@ -1,67 +1,105 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * iTermBot: ReAct agent (LangChain) + Deep agent (DeepAgents).
3
+ * iTermBot: ReAct agent powered by @easynet/agent-runtime.
4
4
  * Usage:
5
- * npm start # interactive: choose agent then chat
6
- * npm run react # use ReAct agent
7
- * npm run deep # use Deep agent
8
- * node dist/index.js react "Your question"
9
- * node dist/index.js deep "Your question"
5
+ * npm start # interactive REPL
6
+ * node dist/index.js "Your question" # single-turn
10
7
  */
8
+ import * as readline from "node:readline";
9
+ import { marked } from "marked";
10
+ import { markedTerminal } from "marked-terminal";
11
11
  import {
12
- runAppCli,
13
- createStructuredRunEventListener,
14
- } from "@easynet/agent-runtime";
15
- import { readFileSync } from "node:fs";
16
- import { dirname, resolve } from "node:path";
17
- import { fileURLToPath } from "node:url";
18
- import type { TargetRoutingState } from "./target-routing.js";
19
- import { applyStartupPanelColors, restoreSessionColorsSync } from "./startup-colors.js";
20
- import { injectTargetSessionHint } from "./session-hint.js";
21
- import { enableDynamicTargetRouting } from "./target-routing.js";
22
- import { createBotContext } from "./context.js";
12
+ createAgentEventBus,
13
+ type AgentEventListener,
14
+ } from "@easynet/agent-common";
15
+ import { resolveConfigPath } from "@easynet/agent-common/config";
16
+ import { createAgentReactRuntime } from "@easynet/agent-runtime";
17
+ import { createAgentModel } from "@easynet/agent-model";
18
+ import { createAgentMemory } from "@easynet/agent-memory";
19
+ import { createAgentTools } from "@easynet/agent-tool";
20
+ import { createAgentSkills } from "@easynet/agent-skill";
21
+ import { runWithTextToolCallRecovery } from "./runtime/text-tool-call-recovery.js";
22
+ import { applyStartupPanelColors, restoreSessionColorsSync } from "./startup/colors.js";
23
+ import { buildSystemPrompt } from "./iterm/session-hint.js";
24
+ import {
25
+ detectDirectTargetPanelCommand,
26
+ tryHandleDirectTargetPanelCommand,
27
+ } from "./iterm/direct-command-router.js";
28
+ import { enforceTargetPanelExecutionPolicy, setTargetPanelHint } from "./iterm/target-panel-policy.js";
29
+ import { runLlmHealthCheck } from "./startup/diagnostics.js";
30
+ import { detectStartupTargetOs } from "./startup/os.js";
31
+ import { createChatProgressEventListener } from "./chat/progress.js";
32
+ import { renderStepLine } from "./chat/step-display.js";
33
+ import { enforceResponseSafetyWithMode } from "./chat/response-safety.js";
34
+ import { tryHandleBuiltinReadCommand } from "./chat/builtin-commands.js";
35
+ import {
36
+ clearStartupNoise,
37
+ formatLogPath,
38
+ getAppVersion,
39
+ printStartupBanner,
40
+ printStartupSummary,
41
+ runStartupStep,
42
+ StartupProgressRenderer,
43
+ } from "./startup/ui.js";
44
+ import { loadAppConfig, type ResponseSafetyMode } from "./config.js";
23
45
 
24
46
  let startupColorSnapshots: Parameters<typeof restoreSessionColorsSync>[0] = [];
25
- let stopDynamicTargetRouting: (() => void) | null = null;
47
+ let unpatchTargetPanelPolicy: (() => void) | null = null;
48
+ type AgentRuntime = Awaited<ReturnType<typeof createAgentReactRuntime>>;
49
+
50
+ marked.use(markedTerminal({
51
+ code: false,
52
+ width: Math.max(80, process.stdout.columns ?? 100),
53
+ reflowText: true,
54
+ heading: (text: string, level: number) => `\n${"#".repeat(level)} ${text}\n`,
55
+ }) as never);
56
+
57
+ function renderMarkdownForTerminal(text: string): string {
58
+ try {
59
+ const rendered = marked.parse(text) as string;
60
+ return rendered.trimEnd();
61
+ } catch {
62
+ return text;
63
+ }
64
+ }
65
+
66
+ let conversationTurn = 0;
67
+
68
+ function supportsColor(): boolean {
69
+ return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
70
+ }
71
+
72
+ function colorize(text: string, code: string): string {
73
+ if (!supportsColor()) return text;
74
+ return `${code}${text}\x1b[0m`;
75
+ }
76
+
77
+ function divider(char = "─"): string {
78
+ const width = Math.max(50, Math.min(process.stdout.columns ?? 100, 120));
79
+ return char.repeat(width);
80
+ }
81
+
82
+ function printUserMessage(turn: number, text: string): void {
83
+ console.log(`\n${colorize(divider("─"), "\x1b[2m")}`);
84
+ console.log(colorize(`[Turn ${turn}] USER`, "\x1b[1m\x1b[36m"));
85
+ console.log(text);
86
+ }
87
+
88
+ function printBotMessage(text: string, turn = conversationTurn): void {
89
+ const rendered = renderMarkdownForTerminal(text);
90
+ console.log(colorize(`[Turn ${turn}] iTermBot`, "\x1b[1m\x1b[32m"));
91
+ console.log(rendered);
92
+ console.log(colorize(divider("─"), "\x1b[2m"));
93
+ }
26
94
 
27
95
  function sanitizeSessionId(input: string): string {
28
96
  return input.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
29
97
  }
30
98
 
31
- function configureThreadMemoryNamespace(ctx: Parameters<NonNullable<Parameters<typeof runAppCli>[0]["onReady"]>>[0], args: {
32
- chatSessionId: string | null;
33
- }): string {
34
- const appConfig = ctx.config.app as
35
- | {
36
- agent?: Record<string, { memory?: { namespace_mode?: "thread" | "fixed"; namespace?: string } }>;
37
- }
38
- | undefined;
39
- const agents = appConfig?.agent;
40
- const defaultBaseNamespace = "user:itermbot";
41
- const baseNamespace =
42
- agents?.react?.memory?.namespace ??
43
- agents?.deep?.memory?.namespace ??
44
- defaultBaseNamespace;
45
- const mode = agents?.react?.memory?.namespace_mode ?? agents?.deep?.memory?.namespace_mode ?? "thread";
46
- const seed = args.chatSessionId ? sanitizeSessionId(args.chatSessionId) : `local_${Date.now().toString(36)}`;
47
- const threadNamespace = mode === "fixed" ? baseNamespace : `${baseNamespace}:thread:${seed}`;
48
-
49
- if (!ctx.config.app) ctx.config.app = {};
50
- if (!ctx.config.app.agent) ctx.config.app.agent = {};
51
-
52
- for (const agentName of ["react", "deep"]) {
53
- const current = ctx.config.app.agent[agentName] ?? {};
54
- const currentMemory = current.memory ?? {};
55
- ctx.config.app.agent[agentName] = {
56
- ...current,
57
- memory: {
58
- ...currentMemory,
59
- namespace: threadNamespace,
60
- },
61
- };
62
- }
63
-
64
- return threadNamespace;
99
+ function buildNamespace(chatSessionId: string | null): string {
100
+ const base = "user:itermbot";
101
+ const seed = chatSessionId ? sanitizeSessionId(chatSessionId) : `local_${Date.now().toString(36)}`;
102
+ return `${base}:thread:${seed}`;
65
103
  }
66
104
 
67
105
  function ensureItermEnvironmentOrExit(): void {
@@ -69,135 +107,307 @@ function ensureItermEnvironmentOrExit(): void {
69
107
  const itermSessionId = process.env.ITERM_SESSION_ID?.trim();
70
108
  const isIterm = termProgram === "iTerm.app" || Boolean(itermSessionId);
71
109
  if (isIterm) return;
72
-
73
110
  console.error("iTermBot startup blocked: iTerm2 environment is required.");
74
111
  console.error("Please run iTermBot inside iTerm2 and try again.");
75
112
  process.exit(1);
76
113
  }
77
114
 
78
- function printStartupBanner(): void {
79
- const lines = [
80
- " _ _____ ____ _ ",
81
- " (_)_ _|__ _ __ _ __ ___ | __ ) ___ | |_ ",
82
- " | | | |/ _ \\ '__| '_ ` _ \\| _ \\ / _ \\| __|",
83
- " | | | | __/ | | | | | | | |_) | (_) | |_ ",
84
- " |_| |_|\\___|_| |_| |_| |_|____/ \\___/ \\__|",
85
- " iTermBot ",
86
- ];
87
- console.log("--------------------------------------------------------------------------------------------------");
88
- console.log(`\n${lines.join("\n")}`);
89
- console.log(" Observe the terminal. Act with precision.\n");
115
+ async function buildRuntime(
116
+ systemPrompt: string,
117
+ namespace: string,
118
+ startup: Awaited<ReturnType<typeof applyStartupPanelColors>>,
119
+ maxSteps?: number,
120
+ ): Promise<AgentRuntime> {
121
+ createAgentEventBus();
122
+ await createAgentModel({
123
+ configPath: resolveConfigPath("config/model.yaml", process.cwd()),
124
+ });
125
+
126
+ const memoryConfigPath = resolveConfigPath("config/memory.yaml", process.cwd());
127
+ await createAgentMemory({ configPath: memoryConfigPath });
128
+ const tools = createAgentTools({
129
+ configFilePath: resolveConfigPath("config/tool.yaml", process.cwd()),
130
+ coreTools: { sandboxRoot: process.cwd() },
131
+ });
132
+ setTargetPanelHint({
133
+ windowId: startup.windowId ?? undefined,
134
+ tabIndex: startup.tabIndex ?? undefined,
135
+ sessionId: startup.targetSessionId ?? undefined,
136
+ });
137
+ if (unpatchTargetPanelPolicy) unpatchTargetPanelPolicy();
138
+ unpatchTargetPanelPolicy = enforceTargetPanelExecutionPolicy(tools);
139
+ await createAgentSkills(resolveConfigPath("config/skill.yaml", process.cwd()));
140
+
141
+ return createAgentReactRuntime({
142
+ systemPrompt,
143
+ namespace,
144
+ ...(typeof maxSteps === "number" ? { maxSteps } : {}),
145
+ });
90
146
  }
91
147
 
92
- function clearStartupNoise(): void {
93
- if (!process.stdout.isTTY) return;
94
- process.stdout.write("\x1b[2J\x1b[H");
148
+ async function runInteractive(runtime: AgentRuntime): Promise<void> {
149
+ const rl = readline.createInterface({
150
+ input: process.stdin,
151
+ output: process.stdout,
152
+ terminal: true,
153
+ });
154
+
155
+ const ask = (): void => {
156
+ rl.question("\nyou> ", async (input) => {
157
+ const trimmed = input.trim();
158
+ if (trimmed === "exit" || trimmed === "quit") {
159
+ rl.close();
160
+ return;
161
+ }
162
+ if (!trimmed) {
163
+ ask();
164
+ return;
165
+ }
166
+ conversationTurn += 1;
167
+ const turn = conversationTurn;
168
+ printUserMessage(turn, trimmed);
169
+ try {
170
+ const builtin = tryHandleBuiltinReadCommand(trimmed, runtime);
171
+ if (builtin !== null) {
172
+ printBotMessage(builtin, turn);
173
+ ask();
174
+ return;
175
+ }
176
+
177
+ const directOutput = await tryHandleDirectCommandWithProgress(trimmed, runtime);
178
+ if (directOutput !== null) {
179
+ printBotMessage(directOutput, turn);
180
+ ask();
181
+ return;
182
+ }
183
+
184
+ const recoveryWriter = printSteps ? writeStepLine : () => {};
185
+ const result = await runWithTextToolCallRecovery(runtime, trimmed, recoveryWriter, {
186
+ windowId: startup.windowId ?? undefined,
187
+ tabIndex: startup.tabIndex ?? undefined,
188
+ sessionId: startup.targetSessionId ?? undefined,
189
+ });
190
+ const safeText = enforceResponseSafetyWithMode(result.text, responseSafetyMode, {
191
+ evidenceText: result.evidenceText,
192
+ targetOs: startupTargetOs,
193
+ });
194
+ if (safeText) printBotMessage(safeText, turn);
195
+ } catch (error) {
196
+ console.error("Error:", error instanceof Error ? error.message : String(error));
197
+ }
198
+ ask();
199
+ });
200
+ };
201
+
202
+ rl.on("close", () => {
203
+ shutdown();
204
+ process.exit(0);
205
+ });
206
+
207
+ ask();
95
208
  }
96
209
 
97
- function getAppVersion(): string {
98
- try {
99
- const currentFile = fileURLToPath(import.meta.url);
100
- const packageJsonPath = resolve(dirname(currentFile), "../package.json");
101
- const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: unknown };
102
- return typeof parsed.version === "string" ? parsed.version : "unknown";
103
- } catch {
104
- return "unknown";
210
+ function shutdown(): void {
211
+ if (unpatchTargetPanelPolicy) {
212
+ unpatchTargetPanelPolicy();
213
+ unpatchTargetPanelPolicy = null;
214
+ }
215
+ setTargetPanelHint(null);
216
+ if (startupColorSnapshots.length > 0) {
217
+ restoreSessionColorsSync(startupColorSnapshots);
105
218
  }
106
219
  }
107
220
 
108
- function printStartupSummary(args: {
109
- version: string;
110
- }): void {
111
- console.log("--------------------------------------------------------------------------------------------------");
112
- console.log(`Version : v${args.version}`);
113
- console.log(`Workspace : ${process.cwd()}`);
114
- console.log("");
115
- console.log("Commands : list tools, list skills, exit");
116
- console.log("--------------------------------------------------------------------------------------------------\n");
117
- }
118
-
119
- ensureItermEnvironmentOrExit();
120
- clearStartupNoise();
121
- printStartupBanner();
122
-
123
- runAppCli({
124
- appName: "iTermBot",
125
- createBotContext,
126
- ui: {
127
- assistantLabel: "iTermBot",
128
- useColor: true,
129
- echoUserQuestion: false,
130
- processingSpinner: true,
131
- processingText: false,
132
- loadingText: "Loading config, LLM, memory, tools...",
133
- loadingSpinner: true,
134
- readyText: false,
135
- interactiveIntro: false,
136
- },
137
- eventListener: createStructuredRunEventListener((line) => console.log(line)),
138
- onShutdown: () => {
139
- if (stopDynamicTargetRouting) {
140
- stopDynamicTargetRouting();
141
- stopDynamicTargetRouting = null;
142
- }
143
- if (startupColorSnapshots.length > 0) {
144
- restoreSessionColorsSync(startupColorSnapshots);
145
- }
146
- },
147
- onReady: async (ctx) => {
148
- const startup = await applyStartupPanelColors();
149
- const routingState: TargetRoutingState = {
150
- chatSessionId: startup.chatSessionId,
151
- chatWindowId: startup.windowId,
152
- chatTabIndex: startup.tabIndex,
153
- targetSessionId: startup.targetSessionId,
154
- windowId: startup.windowId,
155
- tabIndex: startup.tabIndex,
156
- };
157
- stopDynamicTargetRouting = enableDynamicTargetRouting(ctx, routingState);
158
- configureThreadMemoryNamespace(ctx, {
159
- chatSessionId: startup.chatSessionId,
160
- });
221
+ process.on("SIGINT", () => { shutdown(); process.exit(0); });
222
+ process.on("SIGTERM", () => { shutdown(); process.exit(0); });
161
223
 
162
- startupColorSnapshots = startup.colorSnapshots;
224
+ let startup: Awaited<ReturnType<typeof applyStartupPanelColors>> = {
225
+ chatSessionId: null,
226
+ targetSessionId: null,
227
+ windowId: null,
228
+ tabIndex: null,
229
+ colorSnapshots: [],
230
+ };
231
+ let responseSafetyMode: ResponseSafetyMode = "balanced";
232
+ let startupTargetOs = "";
233
+ let printSteps = false;
234
+ let maxSteps = 16;
163
235
 
164
- printStartupSummary({
165
- version: getAppVersion(),
166
- });
167
- injectTargetSessionHint(ctx, {
168
- ...startup,
169
- targetSessionId: routingState.targetSessionId,
170
- windowId: routingState.windowId,
171
- tabIndex: routingState.tabIndex,
236
+ function formatStepNumber(stepNumber: number): string {
237
+ return String(Math.max(0, stepNumber)).padStart(2, "0");
238
+ }
239
+
240
+ function writeStepLine(line: string): void {
241
+ console.log(renderStepLine(line));
242
+ }
243
+
244
+ async function tryHandleDirectCommandWithProgress(
245
+ input: string,
246
+ runtime: AgentRuntime,
247
+ ): Promise<string | null> {
248
+ const verbose = process.env.ITB_VERBOSE_DIRECT_ROUTE?.trim() === "1" || printSteps;
249
+ const startedAt = Date.now();
250
+ const writeStep = (line: string): void => console.log(renderStepLine(line));
251
+ if (verbose) {
252
+ writeStep("");
253
+ writeStep("=== Steps: direct command routing ===");
254
+ writeStep(`[${formatStepNumber(1)}] ▶ detect/translate direct command`);
255
+ }
256
+ try {
257
+ const directIntent = detectDirectTargetPanelCommand(input);
258
+ if (verbose && directIntent?.command) {
259
+ const reason = directIntent.reason === "raw_shell_command"
260
+ ? "Because your input is an executable shell command, execute it directly in the target panel."
261
+ : "Because you explicitly requested direct terminal execution, execute it directly in the target panel.";
262
+ writeStep(` reason: ${reason}`);
263
+ }
264
+
265
+ const directOutput = await tryHandleDirectTargetPanelCommand(input, runtime, {
266
+ windowId: startup.windowId ?? undefined,
267
+ tabIndex: startup.tabIndex ?? undefined,
268
+ sessionId: startup.targetSessionId ?? undefined,
172
269
  });
173
- },
174
- interactiveCommands: {
175
- "list tools": (ctx) => {
176
- const tools = (ctx.tools as Array<{ name?: unknown; description?: unknown }>)
177
- .map((tool) => ({
178
- name: typeof tool.name === "string" ? tool.name : "(unnamed-tool)",
179
- description:
180
- typeof tool.description === "string" && tool.description.trim().length > 0
181
- ? tool.description.trim()
182
- : "No description",
183
- }))
184
- .sort((a, b) => a.name.localeCompare(b.name));
185
- console.log(`Available tools (${tools.length}):`);
186
- for (const tool of tools) {
187
- console.log(`- ${tool.name}: ${tool.description}`);
188
- }
189
- },
190
- "list skills": (ctx) => {
191
- const skills = (ctx.skillSet?.list() ?? [])
192
- .map((skill) => ({
193
- name: skill.name,
194
- description: (skill.description ?? "").trim() || "No description",
195
- }))
196
- .sort((a, b) => a.name.localeCompare(b.name));
197
- console.log(`Available skills (${skills.length}):`);
198
- for (const skill of skills) {
199
- console.log(`- ${skill.name}: ${skill.description}`);
270
+ if (directOutput === null) {
271
+ if (verbose) {
272
+ writeStep(`[${formatStepNumber(1)}] detect/translate direct command (fallback to agent)`);
273
+ writeStep(" progress 1/1");
274
+ writeStep("=== Steps complete: 1 step(s) ===");
275
+ writeStep("");
200
276
  }
201
- },
202
- },
277
+ return null;
278
+ }
279
+ if (verbose) {
280
+ const elapsed = Date.now() - startedAt;
281
+ writeStep(`[${formatStepNumber(1)}] ✓ detect/translate direct command (${elapsed}ms)`);
282
+ writeStep(" progress 1/1");
283
+ writeStep(`=== Steps complete: 1 step(s), ${elapsed}ms ===`);
284
+ writeStep("");
285
+ }
286
+ return directOutput;
287
+ } catch (error) {
288
+ const message = error instanceof Error ? error.message : String(error);
289
+ if (verbose) {
290
+ const elapsed = Date.now() - startedAt;
291
+ writeStep(`[${formatStepNumber(1)}] ✖ detect/translate direct command`);
292
+ writeStep(` error: ${message}`);
293
+ writeStep(" progress 1/1");
294
+ writeStep(`=== Steps complete: 1 step(s), ${elapsed}ms ===`);
295
+ writeStep("");
296
+ }
297
+ throw error;
298
+ }
299
+ }
300
+
301
+ async function main(): Promise<void> {
302
+ ensureItermEnvironmentOrExit();
303
+ clearStartupNoise();
304
+ printStartupBanner();
305
+
306
+ const progress = new StartupProgressRenderer();
307
+
308
+ // 1. Load app config (prompt templates only)
309
+ const appConfigPath = resolveConfigPath("config/app.yaml", process.cwd());
310
+ const appConfig = await runStartupStep(
311
+ progress,
312
+ "init app config",
313
+ () => loadAppConfig(appConfigPath),
314
+ () => formatLogPath(appConfigPath),
315
+ );
316
+ printSteps = appConfig.printSteps === true;
317
+ maxSteps = appConfig.maxSteps ?? 16;
318
+ responseSafetyMode = appConfig.responseSafetyMode ?? "balanced";
319
+
320
+ // 2. Identify iTerm panels and apply colors
321
+ startup = await runStartupStep(
322
+ progress,
323
+ "init iTerm panels",
324
+ () => applyStartupPanelColors(),
325
+ );
326
+ startupColorSnapshots = startup.colorSnapshots;
327
+
328
+ // 3. Detect target panel OS early so command translation can be OS-aware.
329
+ const osInfo = await runStartupStep(
330
+ progress,
331
+ "detect target os",
332
+ () => detectStartupTargetOs({
333
+ windowId: startup.windowId ?? undefined,
334
+ tabIndex: startup.tabIndex ?? undefined,
335
+ sessionId: startup.targetSessionId ?? undefined,
336
+ }),
337
+ (info) => `${info.os} (${info.source})`,
338
+ );
339
+ startupTargetOs = osInfo.os;
340
+ process.env.ITB_TARGET_OS = osInfo.os;
341
+
342
+ // 4. Build system prompt and namespace
343
+ const namespace = buildNamespace(startup.chatSessionId);
344
+ const systemPrompt = buildSystemPrompt(appConfig.promptTemplates, startup, "", {
345
+ targetOs: startupTargetOs,
346
+ });
347
+
348
+ // 5. Create agent runtime (model/memory/tools registered by application)
349
+ let runtime!: AgentRuntime;
350
+ await runStartupStep(
351
+ progress,
352
+ "init agent runtime",
353
+ async () => { runtime = await buildRuntime(systemPrompt, namespace, startup, maxSteps); },
354
+ () => `namespace=${namespace}`,
355
+ );
356
+
357
+ // 6. Subscribe to agent events for UI output
358
+ if (printSteps) {
359
+ const listener: AgentEventListener = createChatProgressEventListener(console.log);
360
+ runtime.events.subscribe(listener);
361
+ }
362
+
363
+ // 7. LLM health check — call LLM directly, not through agent loop
364
+ await runStartupStep(
365
+ progress,
366
+ "check llm health",
367
+ () => runLlmHealthCheck(runtime),
368
+ (label) => label,
369
+ );
370
+
371
+ printStartupSummary({ version: getAppVersion(), responseSafetyMode, targetOs: startupTargetOs });
372
+
373
+ // 8. Single-turn or interactive REPL
374
+ const args = process.argv.slice(2);
375
+ const question = args.find((a) => !a.startsWith("-") && a !== "react" && a !== "deep");
376
+ if (question) {
377
+ conversationTurn += 1;
378
+ const turn = conversationTurn;
379
+ printUserMessage(turn, question);
380
+ const builtin = tryHandleBuiltinReadCommand(question, runtime);
381
+ if (builtin !== null) {
382
+ printBotMessage(builtin, turn);
383
+ shutdown();
384
+ return;
385
+ }
386
+
387
+ const directOutput = await tryHandleDirectCommandWithProgress(question, runtime);
388
+ if (directOutput !== null) {
389
+ printBotMessage(directOutput, turn);
390
+ } else {
391
+ const recoveryWriter = printSteps ? writeStepLine : () => {};
392
+ const result = await runWithTextToolCallRecovery(runtime, question, recoveryWriter, {
393
+ windowId: startup.windowId ?? undefined,
394
+ tabIndex: startup.tabIndex ?? undefined,
395
+ sessionId: startup.targetSessionId ?? undefined,
396
+ });
397
+ const safeText = enforceResponseSafetyWithMode(result.text, responseSafetyMode, {
398
+ evidenceText: result.evidenceText,
399
+ targetOs: startupTargetOs,
400
+ });
401
+ if (safeText) printBotMessage(safeText, turn);
402
+ }
403
+ shutdown();
404
+ } else {
405
+ await runInteractive(runtime);
406
+ }
407
+ }
408
+
409
+ main().catch((error) => {
410
+ console.error(error instanceof Error ? error.message : String(error));
411
+ shutdown();
412
+ process.exit(1);
203
413
  });