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/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "itermbot",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "iTermBot: ReAct agent (LangChain) + DeepAgent (DeepAgents) using @easynet framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
+ "bin": {
8
+ "itermbot": "dist/index.js",
9
+ "iTermBot": "dist/index.js",
10
+ "iTermbot": "dist/index.js"
11
+ },
7
12
  "scripts": {
13
+ "preinstall": "node scripts/resolve-deps.js",
8
14
  "build": "tsc -p config/tsconfig.json",
9
15
  "dev": "tsc -p config/tsconfig.json --watch",
10
16
  "start": "node dist/index.js",
@@ -12,32 +18,36 @@
12
18
  "deep": "node dist/index.js deep",
13
19
  "typecheck": "tsc -p config/tsconfig.json --noEmit",
14
20
  "lint": "tsc -p config/tsconfig.json --noEmit",
15
- "test": "npm run typecheck",
21
+ "test": "npm run typecheck && npm run build && npm run test:policy",
22
+ "test:policy": "npm run build && node --test --test-concurrency=1 test/*.test.mjs",
23
+ "test:live": "npm run build && node --test --test-concurrency=1 test/live-iterm-llm.integration.test.mjs",
16
24
  "test:llm": "node scripts/test-llm.mjs",
17
25
  "release": "semantic-release"
18
26
  },
19
27
  "dependencies": {
20
28
  "@easynet/agent-common": "latest",
21
- "@easynet/agent-model": "latest",
22
29
  "@easynet/agent-memory": "latest",
30
+ "@easynet/agent-model": "latest",
31
+ "@easynet/agent-runtime": "latest",
32
+ "@easynet/agent-skill": "latest",
23
33
  "@easynet/agent-tool": "latest",
24
34
  "@easynet/agent-tool-buildin": "latest",
25
- "@langchain/core": "latest",
26
- "@langchain/langgraph": "latest",
27
- "@langchain/langgraph-checkpoint": "latest",
28
- "deepagents": "latest",
29
- "langchain": "latest",
30
- "zod": "latest",
31
- "@easynet/agent-runtime": "latest"
35
+ "@langchain/core": "1.1.27",
36
+ "@langchain/langgraph": "1.1.5",
37
+ "@langchain/langgraph-checkpoint": "1.0.0",
38
+ "better-sqlite3": "^12.6.2",
39
+ "deepagents": "1.8.0",
40
+ "langchain": "1.2.25",
41
+ "zod": "latest"
32
42
  },
33
43
  "devDependencies": {
34
- "@types/node": "^22.10.0",
35
- "typescript": "~5.7.2",
36
44
  "@semantic-release/commit-analyzer": "^13.0.0",
37
45
  "@semantic-release/git": "^10.0.1",
38
46
  "@semantic-release/npm": "^12.0.0",
39
47
  "@semantic-release/release-notes-generator": "^14.0.0",
40
- "semantic-release": "^24.2.0"
48
+ "@types/node": "^22.10.0",
49
+ "semantic-release": "^24.2.0",
50
+ "typescript": "~5.7.2"
41
51
  },
42
52
  "engines": {
43
53
  "node": ">=18.0.0"
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
4
+ import { dirname, relative, resolve, sep } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const INTERNAL_SCOPE = "@easynet/";
8
+ const DEP_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
9
+
10
+ function isIgnoredDir(name) {
11
+ return name === "node_modules" || name === ".git" || name.startsWith(".");
12
+ }
13
+
14
+ function isInternalPackageName(name) {
15
+ return typeof name === "string" && name.startsWith(INTERNAL_SCOPE);
16
+ }
17
+
18
+ function readJson(path) {
19
+ return JSON.parse(readFileSync(path, "utf8"));
20
+ }
21
+
22
+ function writeJson(path, value) {
23
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
24
+ }
25
+
26
+ function findRepoRoot(startDir) {
27
+ let current = resolve(startDir);
28
+ while (true) {
29
+ const candidate = resolve(current, "agent-runtime");
30
+ if (existsSync(candidate)) return current;
31
+ const gitDir = resolve(current, ".git");
32
+ const packageJson = resolve(current, "package.json");
33
+ if (existsSync(gitDir) && existsSync(packageJson)) return current;
34
+ const parent = dirname(current);
35
+ if (parent === current) {
36
+ return resolve(startDir);
37
+ }
38
+ current = parent;
39
+ }
40
+ }
41
+
42
+ export function collectPackageJsonPaths(rootDir) {
43
+ const out = [];
44
+ function walk(currentDir) {
45
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
46
+ if (entry.isDirectory()) {
47
+ if (isIgnoredDir(entry.name)) continue;
48
+ walk(resolve(currentDir, entry.name));
49
+ continue;
50
+ }
51
+ if (entry.name !== "package.json") continue;
52
+ const fullPath = resolve(currentDir, entry.name);
53
+ if (fullPath.includes(`${sep}test${sep}fixtures${sep}`)) continue;
54
+ out.push(fullPath);
55
+ }
56
+ }
57
+ walk(rootDir);
58
+ return out.sort();
59
+ }
60
+
61
+ export function buildInternalPackageMap(repoRoot) {
62
+ const packageJsonPaths = collectPackageJsonPaths(repoRoot);
63
+ const map = new Map();
64
+ for (const pkgPath of packageJsonPaths) {
65
+ const pkg = readJson(pkgPath);
66
+ if (!isInternalPackageName(pkg.name)) continue;
67
+ map.set(pkg.name, {
68
+ name: pkg.name,
69
+ version: pkg.version,
70
+ packageJsonPath: pkgPath,
71
+ dir: dirname(pkgPath),
72
+ });
73
+ }
74
+ return map;
75
+ }
76
+
77
+ function normalizeFileRef(fromDir, toDir) {
78
+ const rel = relative(fromDir, toDir) || ".";
79
+ return `file:${rel.split(sep).join("/")}`;
80
+ }
81
+
82
+ function resolvePublishFallbackVersion(name, current) {
83
+ if (!isInternalPackageName(name)) return current;
84
+ if (typeof current === "string" && !current.startsWith("file:")) return current;
85
+ return "latest";
86
+ }
87
+
88
+ export function syncPackageJsonRefs(pkgPath, options = {}) {
89
+ const mode = options.mode ?? "auto";
90
+ const repoRoot = options.repoRoot ?? findRepoRoot(dirname(pkgPath));
91
+ const internalPackages = options.internalPackages ?? buildInternalPackageMap(repoRoot);
92
+ const resolvedMode = mode === "auto" ? (process.env.CI ? "publish" : "file") : mode;
93
+ if (resolvedMode !== "file" && resolvedMode !== "publish") {
94
+ throw new Error(`Unsupported sync mode: ${resolvedMode}`);
95
+ }
96
+
97
+ const pkg = readJson(pkgPath);
98
+ let changed = false;
99
+ const pkgDir = dirname(pkgPath);
100
+
101
+ for (const section of DEP_SECTIONS) {
102
+ const deps = pkg[section];
103
+ if (!deps || typeof deps !== "object") continue;
104
+ for (const [name, current] of Object.entries(deps)) {
105
+ if (!isInternalPackageName(name)) continue;
106
+ const internal = internalPackages.get(name);
107
+ const nextValue = internal
108
+ ? (resolvedMode === "file"
109
+ ? normalizeFileRef(pkgDir, internal.dir)
110
+ : internal.version)
111
+ : (resolvedMode === "publish"
112
+ ? resolvePublishFallbackVersion(name, current)
113
+ : current);
114
+ if (current === nextValue) continue;
115
+ deps[name] = nextValue;
116
+ changed = true;
117
+ console.log(`[internal-package-refs] ${pkg.name} ${section}.${name}: ${current} -> ${nextValue}`);
118
+ }
119
+ }
120
+
121
+ if (changed) writeJson(pkgPath, pkg);
122
+ return { changed, mode: resolvedMode, pkgName: pkg.name ?? pkgPath };
123
+ }
124
+
125
+ export function checkWorkspacePackageRefs(repoRoot) {
126
+ const internalPackages = buildInternalPackageMap(repoRoot);
127
+ const failures = [];
128
+ for (const pkgPath of collectPackageJsonPaths(repoRoot)) {
129
+ const pkg = readJson(pkgPath);
130
+ for (const section of DEP_SECTIONS) {
131
+ const deps = pkg[section];
132
+ if (!deps || typeof deps !== "object") continue;
133
+ for (const [name, version] of Object.entries(deps)) {
134
+ if (!isInternalPackageName(name)) continue;
135
+ if (typeof version !== "string") continue;
136
+ if (version === "latest") {
137
+ failures.push(`${pkg.name}: ${section}.${name} must not use "latest"`);
138
+ continue;
139
+ }
140
+ const internal = internalPackages.get(name);
141
+ if (!internal) continue;
142
+ const expectedFile = normalizeFileRef(dirname(pkgPath), internal.dir);
143
+ if (version.startsWith("file:")) {
144
+ if (version !== expectedFile) {
145
+ failures.push(`${pkg.name}: ${section}.${name} should use ${expectedFile}, found ${version}`);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ return failures;
152
+ }
153
+
154
+ export function syncCurrentPackageFromScript(scriptUrl, mode = "auto") {
155
+ const scriptDir = dirname(fileURLToPath(scriptUrl));
156
+ const pkgPath = resolve(scriptDir, "..", "package.json");
157
+ return syncPackageJsonRefs(pkgPath, { mode });
158
+ }
@@ -16,10 +16,7 @@ if [ ! -d "$CACHE_DIR/dist" ]; then
16
16
  exit 1
17
17
  fi
18
18
 
19
- cp "$LOCAL_DIR/dist/src/iterm/itermRunCommandInSession.js" "$CACHE_DIR/dist/src/iterm/"
20
- cp "$LOCAL_DIR/dist/src/iterm/itermRunCommandInSession.d.ts" "$CACHE_DIR/dist/src/iterm/" 2>/dev/null || true
21
- cp "$LOCAL_DIR/dist/src/iterm/common.js" "$CACHE_DIR/dist/src/iterm/"
22
- cp "$LOCAL_DIR/dist/src/iterm/common.d.ts" "$CACHE_DIR/dist/src/iterm/" 2>/dev/null || true
19
+ cp -r "$LOCAL_DIR/dist/src/iterm/" "$CACHE_DIR/dist/src/"
23
20
  cp "$LOCAL_DIR/dist/core-tools-manifest.json" "$CACHE_DIR/dist/"
24
21
 
25
22
  echo "Patched cache at $CACHE_DIR with local build v$VERSION"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { syncCurrentPackageFromScript } from "./internal-package-refs.mjs";
4
+
5
+ syncCurrentPackageFromScript(import.meta.url, "auto");
@@ -1,15 +1,21 @@
1
1
  /**
2
2
  * Quick LLM config test. Run from app root: npm run test:llm
3
3
  */
4
- import { createAgentLlm } from "@easynet/agent-model";
4
+ import { AgentContextTokens, getDefaultAgentContext } from "@easynet/agent-common/context";
5
+ import { createAgentModel } from "@easynet/agent-model";
6
+ import { loadAppConfig, getModelsConfigPath } from "../dist/config.js";
5
7
 
6
8
  async function main() {
7
9
  try {
8
- console.log("[test-llm] Loading LLM from config/models.yaml ...");
9
- const llm = await createAgentLlm({
10
- configPath: "config/models.yaml",
11
- checkConnectivity: false,
10
+ const appConfig = await loadAppConfig("config/app.yaml");
11
+ const agentNames = Object.keys(appConfig?.app?.agent ?? {});
12
+ const agentName = appConfig?.app?.defaultAgent ?? agentNames[0];
13
+ const modelPath = getModelsConfigPath(appConfig, agentName);
14
+ console.log(`[test-llm] Loading LLM from ${modelPath} ...`);
15
+ await createAgentModel({
16
+ configPath: modelPath,
12
17
  });
18
+ const llm = getDefaultAgentContext().get(AgentContextTokens.ChatModel);
13
19
 
14
20
  console.log("[test-llm] Invoking LLM ...");
15
21
  const res = await llm.invoke(
@@ -16,18 +16,33 @@ Use this skill when the user asks to check remote NVIDIA GPU status on `boqiang@
16
16
  - Run all commands in the iTerm target panel via `itermRunCommandInSession`.
17
17
  - Do not use local machine commands for data collection.
18
18
  - Show exact terminal evidence before summary.
19
+ - Do not run `nvidia-smi` before SSH context is confirmed on `ssh.easynet.world`.
19
20
 
20
21
  ## Workflow
21
22
 
22
23
  1. Connect remote host:
23
24
  - `ssh boqiang@ssh.easynet.world`
24
- 2. Verify GPU command availability:
25
+ 2. Confirm remote context before any GPU command:
26
+ - `hostname && whoami`
27
+ - Must show remote host/user evidence consistent with `ssh.easynet.world` / `boqiang`.
28
+ - If still local shell, stop and ask user to complete SSH login in target panel.
29
+ 3. Verify GPU command availability:
25
30
  - `nvidia-smi`
26
- 3. Start live monitor:
31
+ - If output contains `command not found` / `not recognized` / missing driver errors:
32
+ - Stop monitoring workflow immediately.
33
+ - Report only failure evidence from terminal output.
34
+ - Suggest next checks on remote host:
35
+ - `which nvidia-smi`
36
+ - `echo $PATH`
37
+ - `uname -a`
38
+ - `cat /etc/os-release`
39
+ - `ls -l /usr/bin/nvidia-smi /usr/local/bin/nvidia-smi 2>/dev/null`
40
+ - Do not report GPU model/memory/utilization fields when `nvidia-smi` is unavailable.
41
+ 4. Start live monitor:
27
42
  - Preferred: `watch -n 1 nvidia-smi`
28
43
  - Fallback when `watch` is unavailable:
29
44
  - `while true; do clear; nvidia-smi; sleep 1; done`
30
- 4. Stop monitor:
45
+ 5. Stop monitor:
31
46
  - `Ctrl+C`
32
47
 
33
48
  ## Reporting format
@@ -38,6 +53,10 @@ Use this skill when the user asks to check remote NVIDIA GPU status on `boqiang@
38
53
  - Memory usage per GPU
39
54
  - GPU utilization and temperature
40
55
  - Any obvious anomaly (e.g. 0 MiB but high util, ECC/Xid errors)
56
+ - If `nvidia-smi` failed, summary must only contain:
57
+ - failure reason from output,
58
+ - one concrete next command,
59
+ - no unrelated environment/file observations.
41
60
 
42
61
  ## Notes
43
62
 
@@ -0,0 +1,70 @@
1
+ import { AgentContextTokens } from "@easynet/agent-common/context";
2
+ import { normalizeToolList, shortToolName } from "@easynet/agent-common/utils";
3
+
4
+ type RuntimeLike = {
5
+ context: {
6
+ get<T>(token: unknown): T;
7
+ };
8
+ };
9
+
10
+ type ToolLike = {
11
+ name: string;
12
+ description?: string;
13
+ };
14
+
15
+ type SkillLike = {
16
+ name: string;
17
+ description?: string;
18
+ };
19
+
20
+ type SkillSetLike = {
21
+ list: () => SkillLike[];
22
+ };
23
+
24
+ function isToolLike(tool: unknown): tool is ToolLike {
25
+ return Boolean(
26
+ tool
27
+ && typeof tool === "object"
28
+ && typeof (tool as { name?: unknown }).name === "string",
29
+ );
30
+ }
31
+
32
+ function safeGet<T>(runtime: RuntimeLike, token: unknown): T | null {
33
+ try {
34
+ return runtime.context.get<T>(token);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function formatTools(runtime: RuntimeLike): string {
41
+ const raw = safeGet<unknown>(runtime, AgentContextTokens.Tools);
42
+ const tools = normalizeToolList<ToolLike>(raw, isToolLike);
43
+ if (tools.length === 0) return "No tools are registered.";
44
+ const lines = [`Available tools (${tools.length}):`];
45
+ for (const tool of tools) {
46
+ const short = shortToolName(tool.name);
47
+ if (tool.description?.trim()) lines.push(`- ${short}: ${tool.description.trim()}`);
48
+ else lines.push(`- ${short}`);
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+
53
+ function formatSkills(runtime: RuntimeLike): string {
54
+ const skillSet = safeGet<SkillSetLike | undefined>(runtime, AgentContextTokens.SkillSet);
55
+ const skills = skillSet?.list?.() ?? [];
56
+ if (skills.length === 0) return "No skills are configured.";
57
+ const lines = [`Available skills (${skills.length}):`];
58
+ for (const skill of skills) {
59
+ if (skill.description?.trim()) lines.push(`- ${skill.name}: ${skill.description.trim()}`);
60
+ else lines.push(`- ${skill.name}`);
61
+ }
62
+ return lines.join("\n");
63
+ }
64
+
65
+ export function tryHandleBuiltinReadCommand(input: string, runtime: RuntimeLike): string | null {
66
+ const normalized = input.trim().toLowerCase().replace(/\s+/g, " ");
67
+ if (normalized === "list tools") return formatTools(runtime);
68
+ if (normalized === "list skills") return formatSkills(runtime);
69
+ return null;
70
+ }
@@ -0,0 +1,26 @@
1
+ import { createProgressAgentEventListener, type AgentEventListener } from "@easynet/agent-common/events";
2
+ import { renderStepLine } from "./step-display.js";
3
+
4
+ function reasonForAction(action: string): string | null {
5
+ if (action.startsWith("run command:")) {
6
+ return "Because we need verifiable terminal evidence, execute this command first.";
7
+ }
8
+ if (action.startsWith("list directory")) {
9
+ return "Because we need to confirm directory structure and contents, list the directory first.";
10
+ }
11
+ if (action.startsWith("read file:") || action === "read file" || action.startsWith("read path:")) {
12
+ return "Because conclusions depend on file content, read the file first.";
13
+ }
14
+ return null;
15
+ }
16
+
17
+ export function createChatProgressEventListener(
18
+ writer: (line: string) => void = console.log,
19
+ ): AgentEventListener {
20
+ return createProgressAgentEventListener({
21
+ writer: (line: string) => writer(renderStepLine(line)),
22
+ runLabelReact: "analysis",
23
+ runLabelDeep: "deep analysis",
24
+ reasonForAction,
25
+ });
26
+ }
@@ -0,0 +1,134 @@
1
+ export type ResponseSafetyMode = "off" | "balanced" | "strict";
2
+ export type ResponseSafetyOptions = {
3
+ evidenceText?: string;
4
+ targetOs?: string;
5
+ };
6
+
7
+ function containsDangerousCommand(text: string): boolean {
8
+ const patterns = [
9
+ /\brm\s+-rf\b/i,
10
+ /\bsudo\s+rm\b/i,
11
+ /\bmkfs(\.| )/i,
12
+ /\bdd\s+if=/i,
13
+ /\bdiskutil\s+erase/i,
14
+ ];
15
+ return patterns.some((re) => re.test(text));
16
+ }
17
+
18
+ function redactDangerousCommands(text: string): string {
19
+ return text
20
+ .replace(/\brm\s+-rf\b/gi, "[redacted-destructive-command]")
21
+ .replace(/\bsudo\s+rm\b/gi, "[redacted-destructive-command]")
22
+ .replace(/\bmkfs(\.| )\S*/gi, "[redacted-destructive-command]")
23
+ .replace(/\bdd\s+if=\S+/gi, "[redacted-destructive-command]")
24
+ .replace(/\bdiskutil\s+erase\S*/gi, "[redacted-destructive-command]");
25
+ }
26
+
27
+ function removeRedactedSuggestionLines(text: string): string {
28
+ const lines = text
29
+ .split("\n")
30
+ .filter((line) => !line.includes("[redacted-destructive-command]"));
31
+ return lines.join("\n").trim();
32
+ }
33
+
34
+ function buildGenericSafeGuidance(): string {
35
+ return [
36
+ "Safety-preserving workflow:",
37
+ "1. Start with non-destructive inspection and validation.",
38
+ "2. Propose reversible actions before irreversible ones.",
39
+ "3. Ask for explicit confirmation before destructive operations.",
40
+ ].join("\n");
41
+ }
42
+
43
+ export function enforceResponseSafety(text: string): string {
44
+ const trimmed = text.trim();
45
+ if (!trimmed) return trimmed;
46
+ return enforceResponseSafetyWithMode(trimmed, "balanced");
47
+ }
48
+
49
+ function extractAbsolutePaths(text: string): string[] {
50
+ // Restrict path tokens to common path chars so we don't capture markup like "<parameter=...>".
51
+ const matches = text.match(/(?:^|[^A-Za-z0-9_])((?:\/[A-Za-z0-9._-]+)+)/g) ?? [];
52
+ const paths = matches
53
+ .map((raw) => raw.replace(/^[^/]+/, "").trim())
54
+ .map((p) => p.replace(/[.?!]+$/, ""))
55
+ .filter((p) => p.startsWith("/") && p.length > 1);
56
+ return Array.from(new Set(paths));
57
+ }
58
+
59
+ function hasEvidencePathOverlap(path: string, evidencePaths: Set<string>): boolean {
60
+ for (const ev of evidencePaths) {
61
+ if (path === ev) return true;
62
+ if (path.startsWith(`${ev}/`)) return true;
63
+ if (ev.startsWith(`${path}/`)) return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ function replaceUngroundedPaths(text: string, options?: ResponseSafetyOptions): { text: string; replaced: number } {
69
+ const evidence = options?.evidenceText?.trim() ?? "";
70
+ if (!evidence) return { text, replaced: 0 };
71
+ const responsePaths = extractAbsolutePaths(text);
72
+ if (responsePaths.length === 0) return { text, replaced: 0 };
73
+ const evidencePaths = new Set(extractAbsolutePaths(evidence));
74
+ if (evidencePaths.size === 0) return { text, replaced: 0 };
75
+
76
+ let out = text;
77
+ let replaced = 0;
78
+ for (const path of responsePaths) {
79
+ if (hasEvidencePathOverlap(path, evidencePaths)) continue;
80
+ const escaped = path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
81
+ const re = new RegExp(escaped, "g");
82
+ if (re.test(out)) {
83
+ out = out.replace(re, "(not detected from evidence)");
84
+ replaced += 1;
85
+ }
86
+ }
87
+
88
+ // Extra guard for macOS: '/home/*' is usually ungrounded unless explicitly present in evidence.
89
+ if ((options?.targetOs ?? "").toLowerCase() === "darwin" && !Array.from(evidencePaths).some((p) => p.startsWith("/home/"))) {
90
+ const homeRe = /\/home\/[A-Za-z0-9._-]+/g;
91
+ if (homeRe.test(out)) {
92
+ out = out.replace(homeRe, "(not detected from evidence)");
93
+ replaced += 1;
94
+ }
95
+ }
96
+ return { text: out, replaced };
97
+ }
98
+
99
+ export function enforceResponseSafetyWithMode(
100
+ text: string,
101
+ mode: ResponseSafetyMode = "balanced",
102
+ options?: ResponseSafetyOptions,
103
+ ): string {
104
+ const trimmed = text.trim();
105
+ if (!trimmed) return trimmed;
106
+ if (mode === "off") return trimmed;
107
+ const grounded = replaceUngroundedPaths(trimmed, options);
108
+ if (!containsDangerousCommand(grounded.text)) {
109
+ if (grounded.replaced === 0) return grounded.text;
110
+ return `${grounded.text}\n\nSafety note: some paths were replaced because they were not found in tool evidence.`;
111
+ }
112
+
113
+ const redacted = redactDangerousCommands(grounded.text);
114
+ if (mode === "balanced") {
115
+ const out = [
116
+ redacted,
117
+ "",
118
+ "Safety note: destructive command suggestions were redacted. Proceed only with explicit confirmation.",
119
+ ].join("\n");
120
+ if (grounded.replaced === 0) return out;
121
+ return `${out}\nSafety note: some paths were replaced because they were not found in tool evidence.`;
122
+ }
123
+
124
+ const cleaned = removeRedactedSuggestionLines(redacted);
125
+ const safetyNote = [
126
+ "Safety note: destructive command suggestions were redacted.",
127
+ "Prefer non-destructive validation first.",
128
+ "Proceed with destructive operations only after explicit confirmation.",
129
+ ].join(" ");
130
+ const body = cleaned.length > 0 ? cleaned : "Potentially destructive suggestions were removed.";
131
+ const strictOut = `${body}\n\n${buildGenericSafeGuidance()}\n\n${safetyNote}`;
132
+ if (grounded.replaced === 0) return strictOut;
133
+ return `${strictOut}\nSafety note: some paths were replaced because they were not found in tool evidence.`;
134
+ }
@@ -0,0 +1,54 @@
1
+ function supportsColor(): boolean {
2
+ return Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
3
+ }
4
+
5
+ function colorize(text: string, code: string): string {
6
+ if (!supportsColor()) return text;
7
+ return `${code}${text}\x1b[0m`;
8
+ }
9
+
10
+ function trimStepPrefix(line: string): string {
11
+ return line.replace(/^ {4}/, "");
12
+ }
13
+
14
+ export function renderStepLine(line: string): string {
15
+ if (!line) return "";
16
+
17
+ if (line.startsWith("=== Steps: ")) {
18
+ return colorize(`╭─ ${line.replace(/^===\s*/, "").replace(/\s*===$/, "")}`, "\x1b[1m\x1b[36m");
19
+ }
20
+ if (line.startsWith("=== Steps complete: ")) {
21
+ return colorize(`╰─ ${line.replace(/^===\s*/, "").replace(/\s*===$/, "")}`, "\x1b[1m\x1b[32m");
22
+ }
23
+
24
+ if (/^\[\d{2}\] ▶ /.test(line)) {
25
+ return colorize(`├─ ${line}`, "\x1b[33m");
26
+ }
27
+ if (/^\[\d{2}\] ✓ /.test(line)) {
28
+ return colorize(`├─ ${line}`, "\x1b[32m");
29
+ }
30
+ if (/^\[\d{2}\] ✖ /.test(line)) {
31
+ return colorize(`├─ ${line}`, "\x1b[31m");
32
+ }
33
+
34
+ if (line.startsWith(" reason: ")) {
35
+ return colorize(`│ ${trimStepPrefix(line)}`, "\x1b[2m");
36
+ }
37
+ if (line.startsWith(" error: ")) {
38
+ return colorize(`│ ${trimStepPrefix(line)}`, "\x1b[31m");
39
+ }
40
+ if (line.startsWith(" progress ")) {
41
+ return colorize(`│ ${trimStepPrefix(line)}`, "\x1b[34m");
42
+ }
43
+ if (line.startsWith(" skill: ")) {
44
+ return colorize(`│ ${trimStepPrefix(line)}`, "\x1b[35m");
45
+ }
46
+ if (line.startsWith(" memory: ")) {
47
+ return colorize(`│ ${trimStepPrefix(line)}`, "\x1b[2m");
48
+ }
49
+ if (line.startsWith(" context: ") || line.startsWith(" planning retry: ")) {
50
+ return colorize(`│ ${trimStepPrefix(line)}`, "\x1b[2m");
51
+ }
52
+
53
+ return line;
54
+ }
@@ -0,0 +1,22 @@
1
+ import { asRecord } from "@easynet/agent-common/utils";
2
+
3
+ export function normalizeToolInvokeResult(raw: unknown): unknown {
4
+ return raw;
5
+ }
6
+
7
+ export function toolResultOutputText(value: unknown): string | null {
8
+ const root = asRecord(value);
9
+ const result = asRecord(root?.result);
10
+ if (typeof result?.output === "string") return result.output;
11
+ if (typeof root?.output === "string") return root.output;
12
+ return null;
13
+ }
14
+
15
+ export function stringifyToolResult(value: unknown): string {
16
+ if (typeof value === "string") return value;
17
+ try {
18
+ return JSON.stringify(value, null, 2);
19
+ } catch {
20
+ return String(value);
21
+ }
22
+ }