@easynet/agent-runtime 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +9 -24
- package/.github/workflows/release.yml +14 -35
- package/agent-runtime/.github/workflows/ci.yml +68 -0
- package/agent-runtime/.github/workflows/release.yml +95 -0
- package/agent-runtime/.releaserc.cjs +26 -0
- package/agent-runtime/config/agent.deep.yaml +25 -0
- package/agent-runtime/config/agent.react.yaml +24 -0
- package/agent-runtime/example/basic-usage.ts +49 -0
- package/agent-runtime/package-lock.json +7740 -0
- package/agent-runtime/package.json +49 -0
- package/agent-runtime/pnpm-lock.yaml +3712 -0
- package/agent-runtime/scripts/resolve-deps.js +54 -0
- package/agent-runtime/src/agents/deep-agent.ts +165 -0
- package/agent-runtime/src/agents/react-agent.helpers.ts +227 -0
- package/agent-runtime/src/agents/react-agent.ts +584 -0
- package/{src → agent-runtime/src/agents}/sub-agent.ts +2 -2
- package/agent-runtime/src/cli/args.ts +15 -0
- package/agent-runtime/src/cli/event-listener.ts +162 -0
- package/agent-runtime/src/cli/interactive.ts +144 -0
- package/agent-runtime/src/cli/runtime.ts +31 -0
- package/agent-runtime/src/cli/spinner.ts +23 -0
- package/agent-runtime/src/cli/terminal-render.ts +322 -0
- package/agent-runtime/src/cli/types.ts +33 -0
- package/agent-runtime/src/cli.ts +134 -0
- package/agent-runtime/src/config/helpers.ts +179 -0
- package/agent-runtime/src/config/index.ts +245 -0
- package/agent-runtime/src/config/types.ts +62 -0
- package/agent-runtime/src/core/context.ts +266 -0
- package/agent-runtime/src/index.ts +55 -0
- package/agent-runtime/tsconfig.json +18 -0
- package/apps/imessagebot/README.md +38 -0
- package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/README.md +33 -0
- package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/package-lock.json +15257 -0
- package/apps/imessagebot/config/.agent/cache/easynet/agent-tool-buildin/0.0.45/package.json +55 -0
- package/apps/imessagebot/config/agents/deep/agent.yaml +31 -0
- package/apps/imessagebot/config/agents/react/agent.yaml +58 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/README.md +33 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/package-lock.json +15457 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.43/package.json +55 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/README.md +33 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/package-lock.json +15257 -0
- package/apps/imessagebot/config/agents/shared/.agent/cache/easynet/agent-tool-buildin/0.0.46/package.json +62 -0
- package/apps/imessagebot/config/agents/shared/memory.yaml +31 -0
- package/apps/imessagebot/config/agents/shared/model.yaml +23 -0
- package/apps/imessagebot/config/agents/shared/tool.yaml +13 -0
- package/apps/imessagebot/config/app.yaml +14 -0
- package/apps/imessagebot/package-lock.json +53695 -0
- package/apps/imessagebot/package.json +41 -0
- package/apps/imessagebot/pnpm-lock.yaml +1589 -0
- package/apps/imessagebot/scripts/resolve-deps.js +41 -0
- package/apps/imessagebot/scripts/test-llm.mjs +27 -0
- package/apps/imessagebot/scripts/validate-tools-config.mjs +174 -0
- package/apps/imessagebot/src/app/config.ts +76 -0
- package/apps/imessagebot/src/app/context.ts +39 -0
- package/apps/imessagebot/src/config.ts +76 -0
- package/apps/imessagebot/src/context.ts +35 -0
- package/apps/imessagebot/src/index.ts +17 -0
- package/apps/imessagebot/tsconfig.json +18 -0
- package/apps/itermbot/.github/workflows/ci.yml +61 -0
- package/apps/itermbot/.github/workflows/release.yml +80 -0
- package/apps/itermbot/.releaserc.cjs +26 -0
- package/apps/itermbot/README.md +82 -0
- package/apps/itermbot/config/app.yaml +29 -0
- package/apps/itermbot/config/tool.yaml +19 -0
- package/apps/itermbot/config/tsconfig.json +18 -0
- package/apps/itermbot/macos_disk_usage_agent_plan.md +244 -0
- package/apps/itermbot/package-lock.json +53697 -0
- package/apps/itermbot/package.json +57 -0
- package/apps/itermbot/pnpm-lock.yaml +3966 -0
- package/apps/itermbot/scripts/patch-buildin-cache.sh +25 -0
- package/apps/itermbot/scripts/resolve-deps.js +41 -0
- package/apps/itermbot/scripts/test-llm.mjs +32 -0
- package/apps/itermbot/skills/command-explain-and-guard/SKILL.md +39 -0
- package/apps/itermbot/skills/command-explain-and-guard/handler.js +86 -0
- package/apps/itermbot/skills/disk-usage-investigate/SKILL.md +44 -0
- package/apps/itermbot/skills/disk-usage-investigate/handler.js +12 -0
- package/apps/itermbot/skills/gpu-ssh-monitor/SKILL.md +64 -0
- package/apps/itermbot/skills/repo-triage/SKILL.md +40 -0
- package/apps/itermbot/skills/repo-triage/handler.js +56 -0
- package/apps/itermbot/skills/test-failure-diagnose/SKILL.md +43 -0
- package/apps/itermbot/skills/test-failure-diagnose/handler.js +107 -0
- package/apps/itermbot/src/app/config.ts +117 -0
- package/apps/itermbot/src/app/context.ts +39 -0
- package/apps/itermbot/src/config.ts +95 -0
- package/apps/itermbot/src/context.ts +35 -0
- package/apps/itermbot/src/index.ts +223 -0
- package/apps/itermbot/src/iterm/session-hint.ts +40 -0
- package/apps/itermbot/src/iterm/target-panel-policy.ts +220 -0
- package/apps/itermbot/src/iterm/target-routing.ts +419 -0
- package/apps/itermbot/src/startup/colors.ts +317 -0
- package/apps/itermbot/src/startup/diagnostics.ts +97 -0
- package/apps/itermbot/src/startup/ui.ts +141 -0
- package/apps/itermbot/test/target-panel-policy.test.mjs +60 -0
- package/config/agent.deep.yaml +25 -0
- package/config/agent.react.yaml +24 -0
- package/dist/agents/deep-agent.d.ts +37 -0
- package/dist/agents/deep-agent.d.ts.map +1 -0
- package/dist/agents/deep-agent.js +115 -0
- package/dist/agents/deep-agent.js.map +1 -0
- package/dist/agents/react-agent.d.ts +40 -0
- package/dist/agents/react-agent.d.ts.map +1 -0
- package/dist/agents/react-agent.helpers.d.ts +40 -0
- package/dist/agents/react-agent.helpers.d.ts.map +1 -0
- package/dist/agents/react-agent.helpers.js +196 -0
- package/dist/agents/react-agent.helpers.js.map +1 -0
- package/dist/agents/react-agent.js +400 -0
- package/dist/agents/react-agent.js.map +1 -0
- package/dist/agents/sub-agent.d.ts +34 -0
- package/dist/agents/sub-agent.d.ts.map +1 -0
- package/dist/agents/sub-agent.js +53 -0
- package/dist/agents/sub-agent.js.map +1 -0
- package/dist/cli/args.d.ts +8 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +9 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/event-listener.d.ts +3 -0
- package/dist/cli/event-listener.d.ts.map +1 -0
- package/dist/cli/event-listener.js +131 -0
- package/dist/cli/event-listener.js.map +1 -0
- package/dist/cli/interactive.d.ts +4 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +118 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/runtime.d.ts +8 -0
- package/dist/cli/runtime.d.ts.map +1 -0
- package/dist/cli/runtime.js +27 -0
- package/dist/cli/runtime.js.map +1 -0
- package/dist/cli/spinner.d.ts +2 -0
- package/dist/cli/spinner.d.ts.map +1 -0
- package/dist/cli/spinner.js +22 -0
- package/dist/cli/spinner.js.map +1 -0
- package/dist/cli/terminal-render.d.ts +7 -0
- package/dist/cli/terminal-render.d.ts.map +1 -0
- package/dist/cli/terminal-render.js +282 -0
- package/dist/cli/terminal-render.js.map +1 -0
- package/dist/cli/types.d.ts +29 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +3 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli.d.ts +4 -41
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -588
- package/dist/cli.js.map +1 -1
- package/dist/config/helpers.d.ts +6 -0
- package/dist/config/helpers.d.ts.map +1 -0
- package/dist/config/helpers.js +164 -0
- package/dist/config/helpers.js.map +1 -0
- package/dist/config/index.d.ts +15 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +160 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/types.d.ts +57 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/context.d.ts +8 -69
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +44 -24
- package/dist/context.js.map +1 -1
- package/dist/core/context.d.ts +66 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +149 -0
- package/dist/core/context.js.map +1 -0
- package/dist/deep-agent.d.ts +5 -2
- package/dist/deep-agent.d.ts.map +1 -1
- package/dist/deep-agent.js +44 -11
- package/dist/deep-agent.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/middleware/malformed-tool-call-middleware.d.ts +8 -0
- package/dist/middleware/malformed-tool-call-middleware.d.ts.map +1 -0
- package/dist/middleware/malformed-tool-call-middleware.js +191 -0
- package/dist/middleware/malformed-tool-call-middleware.js.map +1 -0
- package/dist/react-agent.d.ts +2 -2
- package/dist/react-agent.d.ts.map +1 -1
- package/dist/react-agent.js +28 -9
- package/dist/react-agent.js.map +1 -1
- package/package.json +1 -1
- package/scripts/resolve-deps.js +54 -0
- package/src/agents/deep-agent.ts +165 -0
- package/src/agents/react-agent.helpers.ts +227 -0
- package/src/agents/react-agent.ts +584 -0
- package/src/agents/sub-agent.ts +82 -0
- package/src/cli/args.ts +15 -0
- package/src/cli/event-listener.ts +162 -0
- package/src/cli/interactive.ts +144 -0
- package/src/cli/runtime.ts +31 -0
- package/src/cli/spinner.ts +23 -0
- package/src/cli/terminal-render.ts +322 -0
- package/src/cli/types.ts +33 -0
- package/src/cli.ts +91 -702
- package/src/config/helpers.ts +179 -0
- package/src/config/index.ts +245 -0
- package/src/config/types.ts +62 -0
- package/src/core/context.ts +266 -0
- package/src/index.ts +13 -11
- package/src/middleware/malformed-tool-call-middleware.ts +239 -0
- package/src/types/markdown-it-terminal.d.ts +4 -0
- package/src/types/marked-terminal.d.ts +16 -0
- package/dist/config.d.ts +0 -86
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -84
- package/dist/config.js.map +0 -1
- package/src/config.ts +0 -177
- package/src/context.ts +0 -247
- package/src/deep-agent.ts +0 -104
- package/src/react-agent.ts +0 -576
- /package/{src → agent-runtime/src/middleware}/malformed-tool-call-middleware.ts +0 -0
- /package/{src → agent-runtime/src/types}/markdown-it-terminal.d.ts +0 -0
- /package/{src → agent-runtime/src/types}/marked-terminal.d.ts +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { BotContext } from "@easynet/agent-runtime";
|
|
2
|
+
|
|
3
|
+
type ToolLike = {
|
|
4
|
+
name?: unknown;
|
|
5
|
+
invoke?: (args: unknown) => Promise<unknown>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const BLOCKED_SHORT_NAMES = new Set([
|
|
9
|
+
"listDir",
|
|
10
|
+
"readText",
|
|
11
|
+
"writeText",
|
|
12
|
+
"runCommand",
|
|
13
|
+
"gitRead",
|
|
14
|
+
"gitAdd",
|
|
15
|
+
"gitCommit",
|
|
16
|
+
"gitDiff",
|
|
17
|
+
"gitPull",
|
|
18
|
+
"gitPush",
|
|
19
|
+
"gitSwitchBranch",
|
|
20
|
+
"gitLogHistory",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
type RedirectableShortName =
|
|
24
|
+
| "listDir"
|
|
25
|
+
| "readText"
|
|
26
|
+
| "runCommand"
|
|
27
|
+
| "itermListWindows"
|
|
28
|
+
| "itermListCurrentWindowSessions"
|
|
29
|
+
| "itermGetSessionInfo";
|
|
30
|
+
|
|
31
|
+
type ItermCommandArgs = {
|
|
32
|
+
command: string;
|
|
33
|
+
waitMs?: number;
|
|
34
|
+
maxOutputLines?: number;
|
|
35
|
+
outputOffsetLines?: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function shortName(name: string): string {
|
|
39
|
+
const parts = name.split(".");
|
|
40
|
+
return parts[parts.length - 1] ?? name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shouldBlockTool(name: string): boolean {
|
|
44
|
+
if (name.includes("itermRunCommandInSession")) return false;
|
|
45
|
+
if (name.includes("iterm")) return true;
|
|
46
|
+
return BLOCKED_SHORT_NAMES.has(shortName(name));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function policyError(toolName: string): Error {
|
|
50
|
+
return new Error(
|
|
51
|
+
`Tool "${toolName}" is blocked in iTermBot policy. ` +
|
|
52
|
+
`Use itermRunCommandInSession on target panel, then analyze returned output.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asRecord(input: unknown): Record<string, unknown> {
|
|
57
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return {};
|
|
58
|
+
return input as Record<string, unknown>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function quoteForBash(value: string): string {
|
|
62
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function asBoolean(value: unknown, fallback: boolean): boolean {
|
|
66
|
+
return typeof value === "boolean" ? value : fallback;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function asBoundedInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
70
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
71
|
+
const normalized = Math.floor(value);
|
|
72
|
+
return Math.max(min, Math.min(max, normalized));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildListDirCommand(args: Record<string, unknown>): ItermCommandArgs {
|
|
76
|
+
const path = typeof args.path === "string" && args.path.trim() ? args.path.trim() : ".";
|
|
77
|
+
const recursive = asBoolean(args.recursive, false);
|
|
78
|
+
const includeHidden = asBoolean(args.includeHidden, false);
|
|
79
|
+
const maxDepth = asBoundedInt(args.maxDepth, 3, 1, 20);
|
|
80
|
+
const maxEntries = asBoundedInt(args.maxEntries, 200, 1, 2000);
|
|
81
|
+
const quotedPath = quoteForBash(path);
|
|
82
|
+
if (!recursive) {
|
|
83
|
+
const lsFlags = includeHidden ? "-la" : "-l";
|
|
84
|
+
return { command: `ls ${lsFlags} ${quotedPath} | head -n ${maxEntries}` };
|
|
85
|
+
}
|
|
86
|
+
const hiddenFilter = includeHidden ? "" : ` | grep -Ev '/\\.[^/]+($|/)'`;
|
|
87
|
+
return {
|
|
88
|
+
command: `find ${quotedPath} -maxdepth ${maxDepth} -print${hiddenFilter} | head -n ${maxEntries}`,
|
|
89
|
+
maxOutputLines: Math.min(maxEntries + 50, 3000),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildReadTextCommand(args: Record<string, unknown>): ItermCommandArgs | null {
|
|
94
|
+
const path = typeof args.path === "string" ? args.path.trim() : "";
|
|
95
|
+
if (!path) return null;
|
|
96
|
+
const maxBytes = asBoundedInt(args.maxBytes, 24_000, 256, 200_000);
|
|
97
|
+
const maxLines = asBoundedInt(Math.floor(maxBytes / 120), 200, 20, 2000);
|
|
98
|
+
return {
|
|
99
|
+
command: `sed -n '1,${maxLines}p' ${quoteForBash(path)}`,
|
|
100
|
+
maxOutputLines: Math.min(maxLines + 40, 2500),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildRunCommandCommand(args: Record<string, unknown>): ItermCommandArgs | null {
|
|
105
|
+
if (typeof args.command === "string" && args.command.trim()) {
|
|
106
|
+
return { command: args.command.trim() };
|
|
107
|
+
}
|
|
108
|
+
if (typeof args.cmd === "string" && args.cmd.trim()) {
|
|
109
|
+
return { command: args.cmd.trim() };
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(args.cmdArray) && args.cmdArray.length > 0) {
|
|
112
|
+
const joined = args.cmdArray.map((part) => String(part)).join(" ").trim();
|
|
113
|
+
if (joined) return { command: joined };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toItermCommandArgs(toolName: string, args: unknown): ItermCommandArgs | null {
|
|
119
|
+
const tool = shortName(toolName);
|
|
120
|
+
const record = asRecord(args);
|
|
121
|
+
if (tool === "listDir") return buildListDirCommand(record);
|
|
122
|
+
if (tool === "readText") return buildReadTextCommand(record);
|
|
123
|
+
if (tool === "runCommand") return buildRunCommandCommand(record);
|
|
124
|
+
if (tool === "itermListWindows") return { command: "pwd && ls -la" };
|
|
125
|
+
if (tool === "itermListCurrentWindowSessions") return { command: "pwd && ls -la" };
|
|
126
|
+
if (tool === "itermGetSessionInfo") return { command: "pwd && ls -la" };
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function isRedirectableTool(toolName: string): toolName is RedirectableShortName {
|
|
131
|
+
return (
|
|
132
|
+
toolName === "listDir" ||
|
|
133
|
+
toolName === "readText" ||
|
|
134
|
+
toolName === "runCommand" ||
|
|
135
|
+
toolName === "itermListWindows" ||
|
|
136
|
+
toolName === "itermListCurrentWindowSessions" ||
|
|
137
|
+
toolName === "itermGetSessionInfo"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function findItermCommandTool(tools: ToolLike[]): ToolLike | null {
|
|
142
|
+
return tools.find((tool) => typeof tool.name === "string" && typeof tool.invoke === "function" && tool.name.includes("itermRunCommandInSession")) ?? null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function invokeRedirect(
|
|
146
|
+
blockedName: string,
|
|
147
|
+
blockedArgs: unknown,
|
|
148
|
+
commandTool: ToolLike | null,
|
|
149
|
+
): Promise<unknown> {
|
|
150
|
+
const mapped = toItermCommandArgs(blockedName, blockedArgs);
|
|
151
|
+
if (!mapped || !commandTool || typeof commandTool.invoke !== "function") {
|
|
152
|
+
throw policyError(blockedName);
|
|
153
|
+
}
|
|
154
|
+
const output = await commandTool.invoke(mapped);
|
|
155
|
+
return {
|
|
156
|
+
result: {
|
|
157
|
+
blockedTool: blockedName,
|
|
158
|
+
redirectedTool: commandTool.name,
|
|
159
|
+
redirected: true,
|
|
160
|
+
originalArgs: asRecord(blockedArgs),
|
|
161
|
+
mappedCommand: mapped.command,
|
|
162
|
+
output,
|
|
163
|
+
},
|
|
164
|
+
evidence: [
|
|
165
|
+
{
|
|
166
|
+
type: "policy",
|
|
167
|
+
ref: "target-panel-policy",
|
|
168
|
+
summary: `Redirected ${blockedName} to ${String(commandTool.name)} with target-panel command execution`,
|
|
169
|
+
createdAt: new Date().toISOString(),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function blockedResult(toolName: string): unknown {
|
|
176
|
+
return {
|
|
177
|
+
result: {
|
|
178
|
+
blocked: true,
|
|
179
|
+
blockedTool: toolName,
|
|
180
|
+
requiredTool: "itermRunCommandInSession",
|
|
181
|
+
message:
|
|
182
|
+
`Tool "${toolName}" is blocked in iTermBot policy. ` +
|
|
183
|
+
`Use itermRunCommandInSession on target panel, then analyze returned output.`,
|
|
184
|
+
suggestedCommand: "pwd && ls -la",
|
|
185
|
+
},
|
|
186
|
+
evidence: [
|
|
187
|
+
{
|
|
188
|
+
type: "policy",
|
|
189
|
+
ref: "target-panel-policy",
|
|
190
|
+
summary: `Blocked ${toolName}; returned policy guidance for target-panel command execution`,
|
|
191
|
+
createdAt: new Date().toISOString(),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function enforceTargetPanelExecutionPolicy(ctx: BotContext): () => void {
|
|
198
|
+
const unpatchFns: Array<() => void> = [];
|
|
199
|
+
const allTools = ctx.tools as unknown as ToolLike[];
|
|
200
|
+
const itermCommandTool = findItermCommandTool(allTools);
|
|
201
|
+
|
|
202
|
+
for (const tool of allTools) {
|
|
203
|
+
if (!tool || typeof tool.name !== "string" || typeof tool.invoke !== "function") continue;
|
|
204
|
+
if (!shouldBlockTool(tool.name)) continue;
|
|
205
|
+
const originalInvoke = tool.invoke.bind(tool);
|
|
206
|
+
tool.invoke = async (args: unknown): Promise<unknown> => {
|
|
207
|
+
const toolShortName = shortName(tool.name as string);
|
|
208
|
+
if (isRedirectableTool(toolShortName)) {
|
|
209
|
+
return invokeRedirect(tool.name as string, args, itermCommandTool);
|
|
210
|
+
}
|
|
211
|
+
return blockedResult(tool.name as string);
|
|
212
|
+
};
|
|
213
|
+
unpatchFns.push(() => {
|
|
214
|
+
tool.invoke = originalInvoke;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return () => {
|
|
218
|
+
for (const unpatch of unpatchFns) unpatch();
|
|
219
|
+
};
|
|
220
|
+
}
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import type { BotContext } from "@easynet/agent-runtime";
|
|
2
|
+
import {
|
|
3
|
+
itermListCurrentWindowSessions,
|
|
4
|
+
itermRename,
|
|
5
|
+
itermSetSessionColors,
|
|
6
|
+
itermSplitPane,
|
|
7
|
+
} from "@easynet/agent-tool-buildin/iterm";
|
|
8
|
+
import {
|
|
9
|
+
CHAT_BG,
|
|
10
|
+
CHAT_FG,
|
|
11
|
+
TARGET_BG,
|
|
12
|
+
TARGET_FG,
|
|
13
|
+
captureSessionColorsByIdSync,
|
|
14
|
+
restoreSessionColorsSync,
|
|
15
|
+
type SessionColorSnapshot,
|
|
16
|
+
} from "../startup/colors.js";
|
|
17
|
+
|
|
18
|
+
interface SessionInfo {
|
|
19
|
+
windowId?: number;
|
|
20
|
+
tabIndex?: number;
|
|
21
|
+
sessionId?: string;
|
|
22
|
+
isCurrentSession?: boolean;
|
|
23
|
+
isCurrentTab?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TargetRoutingState {
|
|
27
|
+
chatSessionId: string | null;
|
|
28
|
+
chatWindowId: number | null;
|
|
29
|
+
chatTabIndex: number | null;
|
|
30
|
+
targetSessionId: string | null;
|
|
31
|
+
windowId: number | null;
|
|
32
|
+
tabIndex: number | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ToolLike = {
|
|
36
|
+
name?: unknown;
|
|
37
|
+
invoke?: (args: unknown) => Promise<unknown>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function enableDynamicTargetRouting(
|
|
41
|
+
ctx: BotContext,
|
|
42
|
+
state: TargetRoutingState,
|
|
43
|
+
options: {
|
|
44
|
+
intervalMs?: number;
|
|
45
|
+
chatTitle?: string;
|
|
46
|
+
targetTitle?: string;
|
|
47
|
+
} = {},
|
|
48
|
+
): () => void {
|
|
49
|
+
const intervalMs = options.intervalMs ?? 2000;
|
|
50
|
+
const chatTitle = options.chatTitle ?? "iTermBot Chat";
|
|
51
|
+
const targetTitle = options.targetTitle ?? "iTermBot Target";
|
|
52
|
+
const unpatchFns: Array<() => void> = [];
|
|
53
|
+
let announcedTarget = state.targetSessionId;
|
|
54
|
+
const originalColors = new Map<string, SessionColorSnapshot>();
|
|
55
|
+
const verboseRoutingLog = process.env.ITERMBOT_ROUTING_LOG === "1";
|
|
56
|
+
|
|
57
|
+
const announceRoutingChange = (message: string): void => {
|
|
58
|
+
// Avoid breaking readline prompt in interactive TTY mode.
|
|
59
|
+
if (!verboseRoutingLog) return;
|
|
60
|
+
console.error(message);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const isLikelyRoutingError = (error: unknown): boolean => {
|
|
64
|
+
const message =
|
|
65
|
+
error instanceof Error
|
|
66
|
+
? error.message
|
|
67
|
+
: typeof error === "string"
|
|
68
|
+
? error
|
|
69
|
+
: "";
|
|
70
|
+
if (!message) return false;
|
|
71
|
+
const lower = message.toLowerCase();
|
|
72
|
+
return (
|
|
73
|
+
lower.includes("session") ||
|
|
74
|
+
lower.includes("window") ||
|
|
75
|
+
lower.includes("tab") ||
|
|
76
|
+
lower.includes("not found") ||
|
|
77
|
+
lower.includes("missing value") ||
|
|
78
|
+
lower.includes("iterm")
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const setSessionTitle = async (
|
|
83
|
+
sessionId: string,
|
|
84
|
+
windowId: number | null,
|
|
85
|
+
tabIndex: number | null,
|
|
86
|
+
sessionName: string,
|
|
87
|
+
): Promise<void> => {
|
|
88
|
+
if (!sessionId) return;
|
|
89
|
+
if (typeof windowId !== "number" || typeof tabIndex !== "number") return;
|
|
90
|
+
try {
|
|
91
|
+
await itermRename({
|
|
92
|
+
windowId,
|
|
93
|
+
tabIndex,
|
|
94
|
+
sessionId,
|
|
95
|
+
sessionName,
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
// non-fatal
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const setSessionColors = async (
|
|
103
|
+
sessionId: string,
|
|
104
|
+
windowId: number | null,
|
|
105
|
+
tabIndex: number | null,
|
|
106
|
+
colors: { backgroundHex: string; foregroundHex: string },
|
|
107
|
+
): Promise<void> => {
|
|
108
|
+
if (!sessionId) return;
|
|
109
|
+
if (typeof windowId !== "number" || typeof tabIndex !== "number") return;
|
|
110
|
+
try {
|
|
111
|
+
await itermSetSessionColors({
|
|
112
|
+
windowId,
|
|
113
|
+
tabIndex,
|
|
114
|
+
sessionId,
|
|
115
|
+
backgroundHex: colors.backgroundHex,
|
|
116
|
+
foregroundHex: colors.foregroundHex,
|
|
117
|
+
});
|
|
118
|
+
} catch {
|
|
119
|
+
// non-fatal
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const rememberOriginalColors = (
|
|
124
|
+
sessionId: string,
|
|
125
|
+
windowId: number | null,
|
|
126
|
+
tabIndex: number | null,
|
|
127
|
+
): void => {
|
|
128
|
+
if (!sessionId) return;
|
|
129
|
+
if (originalColors.has(sessionId)) return;
|
|
130
|
+
if (typeof windowId !== "number" || typeof tabIndex !== "number") return;
|
|
131
|
+
const snapshot = captureSessionColorsByIdSync({
|
|
132
|
+
windowId,
|
|
133
|
+
tabIndex,
|
|
134
|
+
sessionId,
|
|
135
|
+
});
|
|
136
|
+
if (!snapshot) return;
|
|
137
|
+
originalColors.set(sessionId, snapshot);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const restoreOriginalColors = (sessionId: string | null): void => {
|
|
141
|
+
if (!sessionId) return;
|
|
142
|
+
const snapshot = originalColors.get(sessionId);
|
|
143
|
+
if (!snapshot) return;
|
|
144
|
+
try {
|
|
145
|
+
restoreSessionColorsSync([snapshot]);
|
|
146
|
+
} catch {
|
|
147
|
+
// non-fatal
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const enforceTitles = async (): Promise<void> => {
|
|
152
|
+
if (state.chatSessionId) {
|
|
153
|
+
await setSessionTitle(state.chatSessionId, state.chatWindowId, state.chatTabIndex, chatTitle);
|
|
154
|
+
await setSessionColors(state.chatSessionId, state.chatWindowId, state.chatTabIndex, {
|
|
155
|
+
backgroundHex: CHAT_BG,
|
|
156
|
+
foregroundHex: CHAT_FG,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (state.targetSessionId) {
|
|
160
|
+
rememberOriginalColors(state.targetSessionId, state.windowId, state.tabIndex);
|
|
161
|
+
await setSessionTitle(state.targetSessionId, state.windowId, state.tabIndex, targetTitle);
|
|
162
|
+
await setSessionColors(state.targetSessionId, state.windowId, state.tabIndex, {
|
|
163
|
+
backgroundHex: TARGET_BG,
|
|
164
|
+
foregroundHex: TARGET_FG,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const chooseFallbackTarget = (sessions: SessionInfo[], chatSessionId: string | null): SessionInfo | null => {
|
|
170
|
+
const bySameTab = sessions.find((s) =>
|
|
171
|
+
Boolean(s.sessionId) &&
|
|
172
|
+
s.sessionId !== chatSessionId &&
|
|
173
|
+
typeof state.chatTabIndex === "number" &&
|
|
174
|
+
s.tabIndex === state.chatTabIndex,
|
|
175
|
+
);
|
|
176
|
+
if (bySameTab) return bySameTab;
|
|
177
|
+
|
|
178
|
+
const byCurrentTab = sessions.find((s) =>
|
|
179
|
+
Boolean(s.sessionId) &&
|
|
180
|
+
s.sessionId !== chatSessionId &&
|
|
181
|
+
s.isCurrentTab,
|
|
182
|
+
);
|
|
183
|
+
if (byCurrentTab) return byCurrentTab;
|
|
184
|
+
|
|
185
|
+
const byAny = sessions.find((s) =>
|
|
186
|
+
Boolean(s.sessionId) &&
|
|
187
|
+
s.sessionId !== chatSessionId,
|
|
188
|
+
);
|
|
189
|
+
return byAny ?? null;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const refreshTargetForCommand = async (opts: {
|
|
193
|
+
allowSplit: boolean;
|
|
194
|
+
adoptFocusedNonChat: boolean;
|
|
195
|
+
}): Promise<{
|
|
196
|
+
changed: boolean;
|
|
197
|
+
autoCreated: boolean;
|
|
198
|
+
}> => {
|
|
199
|
+
const { result } = await itermListCurrentWindowSessions();
|
|
200
|
+
const sessions = (result.sessions ?? []) as SessionInfo[];
|
|
201
|
+
if (sessions.length === 0) {
|
|
202
|
+
return { changed: false, autoCreated: false };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const current = sessions.find((s) => s.isCurrentSession && s.sessionId) ?? null;
|
|
206
|
+
const chatSession =
|
|
207
|
+
sessions.find((s) => s.sessionId && state.chatSessionId && s.sessionId === state.chatSessionId) ??
|
|
208
|
+
current ??
|
|
209
|
+
sessions.find((s) => Boolean(s.sessionId)) ??
|
|
210
|
+
null;
|
|
211
|
+
|
|
212
|
+
if (chatSession?.sessionId) {
|
|
213
|
+
const prevChatSessionId = state.chatSessionId;
|
|
214
|
+
state.chatSessionId = chatSession.sessionId;
|
|
215
|
+
if (typeof chatSession.windowId === "number") state.chatWindowId = chatSession.windowId;
|
|
216
|
+
if (typeof chatSession.tabIndex === "number") state.chatTabIndex = chatSession.tabIndex;
|
|
217
|
+
if (prevChatSessionId !== state.chatSessionId) {
|
|
218
|
+
state.targetSessionId = null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (opts.adoptFocusedNonChat && current?.sessionId && current.sessionId !== state.chatSessionId) {
|
|
223
|
+
const changed = state.targetSessionId !== current.sessionId;
|
|
224
|
+
state.targetSessionId = current.sessionId;
|
|
225
|
+
if (typeof current.windowId === "number") state.windowId = current.windowId;
|
|
226
|
+
if (typeof current.tabIndex === "number") state.tabIndex = current.tabIndex;
|
|
227
|
+
return { changed, autoCreated: false };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const currentTarget = sessions.find((s) => s.sessionId && s.sessionId === state.targetSessionId) ?? null;
|
|
231
|
+
const targetIsValid = Boolean(
|
|
232
|
+
currentTarget?.sessionId &&
|
|
233
|
+
(!state.chatSessionId || currentTarget.sessionId !== state.chatSessionId),
|
|
234
|
+
);
|
|
235
|
+
if (targetIsValid) {
|
|
236
|
+
if (currentTarget && typeof currentTarget.windowId === "number") state.windowId = currentTarget.windowId;
|
|
237
|
+
if (currentTarget && typeof currentTarget.tabIndex === "number") state.tabIndex = currentTarget.tabIndex;
|
|
238
|
+
return { changed: false, autoCreated: false };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const fallback = chooseFallbackTarget(sessions, state.chatSessionId);
|
|
242
|
+
if (fallback?.sessionId) {
|
|
243
|
+
const changed = state.targetSessionId !== fallback.sessionId;
|
|
244
|
+
state.targetSessionId = fallback.sessionId;
|
|
245
|
+
if (typeof fallback.windowId === "number") state.windowId = fallback.windowId;
|
|
246
|
+
if (typeof fallback.tabIndex === "number") state.tabIndex = fallback.tabIndex;
|
|
247
|
+
return { changed, autoCreated: false };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!opts.allowSplit || !chatSession?.sessionId) {
|
|
251
|
+
return { changed: false, autoCreated: false };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const split = await itermSplitPane({
|
|
255
|
+
windowId: typeof chatSession.windowId === "number" ? chatSession.windowId : undefined,
|
|
256
|
+
tabIndex: typeof chatSession.tabIndex === "number" ? chatSession.tabIndex : undefined,
|
|
257
|
+
sessionId: chatSession.sessionId,
|
|
258
|
+
direction: "vertical",
|
|
259
|
+
activate: false,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const splitSessionId = typeof split?.result?.sessionId === "string" ? split.result.sessionId : "";
|
|
263
|
+
if (splitSessionId && splitSessionId !== chatSession.sessionId) {
|
|
264
|
+
const changed = state.targetSessionId !== splitSessionId;
|
|
265
|
+
state.targetSessionId = splitSessionId;
|
|
266
|
+
if (typeof split.result.windowId === "number") state.windowId = split.result.windowId;
|
|
267
|
+
if (typeof split.result.tabIndex === "number") state.tabIndex = split.result.tabIndex;
|
|
268
|
+
return { changed, autoCreated: true };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const refreshed = await itermListCurrentWindowSessions();
|
|
272
|
+
const refreshedSessions = (refreshed.result.sessions ?? []) as SessionInfo[];
|
|
273
|
+
const refreshedFallback = chooseFallbackTarget(refreshedSessions, state.chatSessionId);
|
|
274
|
+
if (refreshedFallback?.sessionId) {
|
|
275
|
+
const changed = state.targetSessionId !== refreshedFallback.sessionId;
|
|
276
|
+
state.targetSessionId = refreshedFallback.sessionId;
|
|
277
|
+
if (typeof refreshedFallback.windowId === "number") state.windowId = refreshedFallback.windowId;
|
|
278
|
+
if (typeof refreshedFallback.tabIndex === "number") state.tabIndex = refreshedFallback.tabIndex;
|
|
279
|
+
return { changed, autoCreated: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return { changed: false, autoCreated: false };
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Initial titles
|
|
286
|
+
void enforceTitles();
|
|
287
|
+
|
|
288
|
+
for (const t of ctx.tools as unknown as ToolLike[]) {
|
|
289
|
+
if (!t || typeof t.name !== "string" || typeof t.invoke !== "function") continue;
|
|
290
|
+
if (!t.name.includes("itermRunCommandInSession")) continue;
|
|
291
|
+
|
|
292
|
+
const originalInvoke = t.invoke.bind(t);
|
|
293
|
+
t.invoke = async (args: unknown): Promise<unknown> => {
|
|
294
|
+
const baseArgs =
|
|
295
|
+
args && typeof args === "object"
|
|
296
|
+
? { ...(args as Record<string, unknown>) }
|
|
297
|
+
: ({} as Record<string, unknown>);
|
|
298
|
+
|
|
299
|
+
const applyRoutingArgs = (target: Record<string, unknown>): void => {
|
|
300
|
+
if (state.targetSessionId) target.sessionId = state.targetSessionId;
|
|
301
|
+
if (typeof state.windowId === "number") target.windowId = state.windowId;
|
|
302
|
+
if (typeof state.tabIndex === "number") target.tabIndex = state.tabIndex;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Fast path: avoid iTerm preflight on every command.
|
|
307
|
+
// Only preflight when routing state is empty.
|
|
308
|
+
if (!state.targetSessionId) {
|
|
309
|
+
const previousTarget = state.targetSessionId;
|
|
310
|
+
const ensureResult = await refreshTargetForCommand({
|
|
311
|
+
allowSplit: true,
|
|
312
|
+
adoptFocusedNonChat: true,
|
|
313
|
+
});
|
|
314
|
+
if (ensureResult.changed) {
|
|
315
|
+
await enforceTitles();
|
|
316
|
+
if (previousTarget && previousTarget !== state.targetSessionId) {
|
|
317
|
+
restoreOriginalColors(previousTarget);
|
|
318
|
+
}
|
|
319
|
+
if (state.targetSessionId !== announcedTarget) {
|
|
320
|
+
announcedTarget = state.targetSessionId;
|
|
321
|
+
if (ensureResult.autoCreated) {
|
|
322
|
+
announceRoutingChange("Session routing updated: target panel auto-created and linked.");
|
|
323
|
+
} else {
|
|
324
|
+
announceRoutingChange("Session routing updated: target panel linked.");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
// keep previous target when preflight fails
|
|
331
|
+
}
|
|
332
|
+
applyRoutingArgs(baseArgs);
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
return await originalInvoke(baseArgs);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (!isLikelyRoutingError(error)) {
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
// Slow path fallback: routing may be stale (target pane closed/moved).
|
|
341
|
+
const previousTarget = state.targetSessionId;
|
|
342
|
+
try {
|
|
343
|
+
const ensureResult = await refreshTargetForCommand({
|
|
344
|
+
allowSplit: true,
|
|
345
|
+
adoptFocusedNonChat: true,
|
|
346
|
+
});
|
|
347
|
+
if (ensureResult.changed) {
|
|
348
|
+
await enforceTitles();
|
|
349
|
+
if (previousTarget && previousTarget !== state.targetSessionId) {
|
|
350
|
+
restoreOriginalColors(previousTarget);
|
|
351
|
+
}
|
|
352
|
+
if (state.targetSessionId !== announcedTarget) {
|
|
353
|
+
announcedTarget = state.targetSessionId;
|
|
354
|
+
if (ensureResult.autoCreated) {
|
|
355
|
+
announceRoutingChange("Session routing updated: target panel auto-created and linked.");
|
|
356
|
+
} else {
|
|
357
|
+
announceRoutingChange("Session routing updated: target panel linked.");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// If reroute fails, fall through and surface original error.
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const retryArgs = { ...baseArgs };
|
|
366
|
+
applyRoutingArgs(retryArgs);
|
|
367
|
+
return originalInvoke(retryArgs);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
unpatchFns.push(() => {
|
|
372
|
+
t.invoke = originalInvoke;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let stopped = false;
|
|
377
|
+
let polling = false;
|
|
378
|
+
const timer = setInterval(() => {
|
|
379
|
+
if (stopped || polling) return;
|
|
380
|
+
polling = true;
|
|
381
|
+
void (async () => {
|
|
382
|
+
try {
|
|
383
|
+
const previousTarget = state.targetSessionId;
|
|
384
|
+
const ensured = await refreshTargetForCommand({
|
|
385
|
+
allowSplit: false,
|
|
386
|
+
adoptFocusedNonChat: true,
|
|
387
|
+
});
|
|
388
|
+
if (ensured.changed) {
|
|
389
|
+
await enforceTitles();
|
|
390
|
+
if (previousTarget && previousTarget !== state.targetSessionId) {
|
|
391
|
+
restoreOriginalColors(previousTarget);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (state.targetSessionId === announcedTarget) return;
|
|
395
|
+
announcedTarget = state.targetSessionId;
|
|
396
|
+
announceRoutingChange("Session routing updated: target panel linked.");
|
|
397
|
+
} catch {
|
|
398
|
+
// keep previous target when polling fails
|
|
399
|
+
} finally {
|
|
400
|
+
polling = false;
|
|
401
|
+
}
|
|
402
|
+
})();
|
|
403
|
+
}, intervalMs);
|
|
404
|
+
|
|
405
|
+
return () => {
|
|
406
|
+
stopped = true;
|
|
407
|
+
clearInterval(timer);
|
|
408
|
+
if (originalColors.size > 0) {
|
|
409
|
+
try {
|
|
410
|
+
restoreSessionColorsSync(Array.from(originalColors.values()));
|
|
411
|
+
} catch {
|
|
412
|
+
// non-fatal
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const unpatch of unpatchFns) {
|
|
416
|
+
unpatch();
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|