agent-sh 0.12.6 → 0.12.8
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/README.md +2 -0
- package/dist/agent/agent-loop.js +2 -1
- package/dist/agent/tools/glob.js +14 -11
- package/dist/agent/tools/grep.js +22 -14
- package/dist/executor.d.ts +17 -0
- package/dist/executor.js +89 -0
- package/dist/extensions/openrouter.js +1 -1
- package/dist/shell/shell.js +1 -0
- package/dist/utils/ripgrep-path.d.ts +7 -0
- package/dist/utils/ripgrep-path.js +18 -0
- package/dist/utils/tool-display.js +2 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -77,6 +77,8 @@ alias ash="agent-sh"
|
|
|
77
77
|
|
|
78
78
|
Requires Node.js 18+. Currently supports **bash** and **zsh**; other shells (fish, nushell, etc.) are not yet wired up.
|
|
79
79
|
|
|
80
|
+
**Windows:** the interactive shell layer is bash/zsh-only. Run agent-sh inside **WSL** for the full experience. Native Windows (cmd.exe / PowerShell) is not supported as the host shell, though headless / library / ACP-bridge usage may work — file an issue if you hit a gap.
|
|
81
|
+
|
|
80
82
|
## Key Features
|
|
81
83
|
|
|
82
84
|
**Real terminal, zero compromise.** Full PTY with your shell config, aliases, and environment. Shell starts instantly — the agent connects asynchronously in the background.
|
package/dist/agent/agent-loop.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { setMaxListeners } from "node:events";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
4
5
|
import { computeDiff, computeEditDiff, computeInputDiff } from "../utils/diff.js";
|
|
5
6
|
import { ToolRegistry } from "./tool-registry.js";
|
|
6
7
|
import { ConversationState } from "./conversation-state.js";
|
|
@@ -915,7 +916,7 @@ export class AgentLoop {
|
|
|
915
916
|
permKind = "file-write";
|
|
916
917
|
// Shorten path for display
|
|
917
918
|
const cwd = process.cwd();
|
|
918
|
-
const home = process.env.HOME;
|
|
919
|
+
const home = process.env.HOME ?? os.homedir();
|
|
919
920
|
let displayPath = absPath;
|
|
920
921
|
if (absPath.startsWith(cwd + "/"))
|
|
921
922
|
displayPath = absPath.slice(cwd.length + 1);
|
package/dist/agent/tools/glob.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { executeArgv } from "../../executor.js";
|
|
4
|
+
import { resolveRgPath } from "../../utils/ripgrep-path.js";
|
|
4
5
|
import { expandHome } from "./expand-home.js";
|
|
5
6
|
export function createGlobTool(getCwd) {
|
|
6
7
|
return {
|
|
@@ -15,11 +16,11 @@ export function createGlobTool(getCwd) {
|
|
|
15
16
|
properties: {
|
|
16
17
|
pattern: {
|
|
17
18
|
type: "string",
|
|
18
|
-
description: "Glob pattern (e.g., 'src/**/*.ts', '*.json')",
|
|
19
|
+
description: "Glob pattern (e.g., 'src/**/*.ts', '*.json'). Do NOT put `~` or absolute prefixes here — pass the directory in `path` instead.",
|
|
19
20
|
},
|
|
20
21
|
path: {
|
|
21
22
|
type: "string",
|
|
22
|
-
description: "Base directory to search (default: cwd)",
|
|
23
|
+
description: "Base directory to search (default: cwd). Supports `~` and `~/...` for the home directory.",
|
|
23
24
|
},
|
|
24
25
|
},
|
|
25
26
|
required: ["pattern"],
|
|
@@ -42,18 +43,20 @@ export function createGlobTool(getCwd) {
|
|
|
42
43
|
const pattern = args.pattern;
|
|
43
44
|
const searchPath = expandHome(args.path ?? ".");
|
|
44
45
|
// Use ripgrep for correct glob matching + .gitignore awareness
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"--glob", shellEsc(pattern),
|
|
49
|
-
shellEsc(searchPath),
|
|
50
|
-
];
|
|
51
|
-
const { session, done } = executeCommand({
|
|
52
|
-
command: parts.join(" "),
|
|
46
|
+
const { session, done } = executeArgv({
|
|
47
|
+
file: resolveRgPath(),
|
|
48
|
+
args: ["--files", "--glob", pattern, searchPath],
|
|
53
49
|
cwd: getCwd(),
|
|
54
50
|
timeout: 10_000,
|
|
55
51
|
});
|
|
56
52
|
await done;
|
|
53
|
+
if (session.exitCode === -1 && session.output.startsWith("Failed to spawn")) {
|
|
54
|
+
return {
|
|
55
|
+
content: "ripgrep not available — the bundled binary failed to load and `rg` is not on PATH. Reinstall agent-sh, or install ripgrep manually (https://github.com/BurntSushi/ripgrep#installation).",
|
|
56
|
+
exitCode: 1,
|
|
57
|
+
isError: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
57
60
|
if (!session.output.trim()) {
|
|
58
61
|
return {
|
|
59
62
|
content: "No files matched.",
|
package/dist/agent/tools/grep.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { executeArgv } from "../../executor.js";
|
|
2
|
+
import { resolveRgPath } from "../../utils/ripgrep-path.js";
|
|
2
3
|
import { expandHome } from "./expand-home.js";
|
|
3
4
|
export function createGrepTool(getCwd) {
|
|
4
5
|
return {
|
|
@@ -89,43 +90,50 @@ export function createGrepTool(getCwd) {
|
|
|
89
90
|
const contextAfter = args.context_after;
|
|
90
91
|
const headLimit = args.head_limit;
|
|
91
92
|
const offset = args.offset ?? 0;
|
|
92
|
-
const
|
|
93
|
-
const parts = ["rg", "--color=never"];
|
|
93
|
+
const rgArgs = ["--color=never"];
|
|
94
94
|
// Mode-specific flags
|
|
95
95
|
if (mode === "files_with_matches") {
|
|
96
|
-
|
|
96
|
+
rgArgs.push("--files-with-matches");
|
|
97
97
|
}
|
|
98
98
|
else if (mode === "count") {
|
|
99
|
-
|
|
99
|
+
rgArgs.push("--count");
|
|
100
100
|
}
|
|
101
101
|
else {
|
|
102
102
|
// content mode
|
|
103
|
-
|
|
103
|
+
rgArgs.push("--line-number", "--no-heading");
|
|
104
104
|
if (contextBefore != null && contextBefore > 0) {
|
|
105
|
-
|
|
105
|
+
rgArgs.push(`-B${contextBefore}`);
|
|
106
106
|
}
|
|
107
107
|
if (contextAfter != null && contextAfter > 0) {
|
|
108
|
-
|
|
108
|
+
rgArgs.push(`-A${contextAfter}`);
|
|
109
109
|
}
|
|
110
110
|
// If neither -A nor -B specified, use --max-count to limit per-file
|
|
111
111
|
if (contextBefore == null && contextAfter == null) {
|
|
112
|
-
|
|
112
|
+
rgArgs.push("--max-count=50");
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
if (caseInsensitive) {
|
|
116
|
-
|
|
116
|
+
rgArgs.push("-i");
|
|
117
117
|
}
|
|
118
118
|
if (include) {
|
|
119
|
-
|
|
119
|
+
rgArgs.push("--glob", include);
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
const { session, done } =
|
|
123
|
-
|
|
121
|
+
rgArgs.push("-e", pattern, searchPath);
|
|
122
|
+
const { session, done } = executeArgv({
|
|
123
|
+
file: resolveRgPath(),
|
|
124
|
+
args: rgArgs,
|
|
124
125
|
cwd: getCwd(),
|
|
125
126
|
timeout: 10_000,
|
|
126
127
|
maxOutputBytes: 64 * 1024,
|
|
127
128
|
});
|
|
128
129
|
await done;
|
|
130
|
+
if (session.exitCode === -1 && session.output.startsWith("Failed to spawn")) {
|
|
131
|
+
return {
|
|
132
|
+
content: "ripgrep not available — the bundled binary failed to load and `rg` is not on PATH. Reinstall agent-sh, or install ripgrep manually (https://github.com/BurntSushi/ripgrep#installation).",
|
|
133
|
+
exitCode: 1,
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
129
137
|
if (session.exitCode === 1 && !session.output.trim()) {
|
|
130
138
|
// If the pattern looks like a filename (e.g. "SKILL.md", "package.json"),
|
|
131
139
|
// the agent probably meant to find files by name, not search inside them.
|
package/dist/executor.d.ts
CHANGED
|
@@ -24,6 +24,23 @@ export declare function executeCommand(opts: {
|
|
|
24
24
|
session: ExecutorSession;
|
|
25
25
|
done: Promise<void>;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Spawn a binary directly (no shell). Use for invoking known tools like `rg`
|
|
29
|
+
* with structured args — avoids shell-quoting bugs and works on platforms
|
|
30
|
+
* without /bin/bash.
|
|
31
|
+
*/
|
|
32
|
+
export declare function executeArgv(opts: {
|
|
33
|
+
file: string;
|
|
34
|
+
args: string[];
|
|
35
|
+
cwd: string;
|
|
36
|
+
env?: Record<string, string>;
|
|
37
|
+
timeout?: number;
|
|
38
|
+
maxOutputBytes?: number;
|
|
39
|
+
onOutput?: (chunk: string) => void;
|
|
40
|
+
}): {
|
|
41
|
+
session: ExecutorSession;
|
|
42
|
+
done: Promise<void>;
|
|
43
|
+
};
|
|
27
44
|
/**
|
|
28
45
|
* Kill a running session's process group: SIGTERM, then SIGKILL after 5s.
|
|
29
46
|
* Returns a cleanup that cancels the pending SIGKILL — callers should invoke
|
package/dist/executor.js
CHANGED
|
@@ -87,6 +87,95 @@ export function executeCommand(opts) {
|
|
|
87
87
|
});
|
|
88
88
|
return { session, done };
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Spawn a binary directly (no shell). Use for invoking known tools like `rg`
|
|
92
|
+
* with structured args — avoids shell-quoting bugs and works on platforms
|
|
93
|
+
* without /bin/bash.
|
|
94
|
+
*/
|
|
95
|
+
export function executeArgv(opts) {
|
|
96
|
+
const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
97
|
+
const maxOutput = opts.maxOutputBytes ?? DEFAULT_MAX_OUTPUT;
|
|
98
|
+
const session = {
|
|
99
|
+
id: "",
|
|
100
|
+
command: `${opts.file} ${opts.args.join(" ")}`,
|
|
101
|
+
output: "",
|
|
102
|
+
exitCode: null,
|
|
103
|
+
done: false,
|
|
104
|
+
truncated: false,
|
|
105
|
+
process: null,
|
|
106
|
+
};
|
|
107
|
+
const done = new Promise((resolve) => {
|
|
108
|
+
session.resolve = resolve;
|
|
109
|
+
});
|
|
110
|
+
const env = {};
|
|
111
|
+
const source = opts.env ?? process.env;
|
|
112
|
+
for (const [k, v] of Object.entries(source)) {
|
|
113
|
+
if (v !== undefined)
|
|
114
|
+
env[k] = v;
|
|
115
|
+
}
|
|
116
|
+
let child;
|
|
117
|
+
try {
|
|
118
|
+
child = spawn(opts.file, opts.args, {
|
|
119
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
120
|
+
cwd: opts.cwd,
|
|
121
|
+
env,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
session.exitCode = -1;
|
|
126
|
+
session.output = `Failed to spawn ${opts.file}: ${err instanceof Error ? err.message : String(err)}`;
|
|
127
|
+
session.done = true;
|
|
128
|
+
session.resolve?.();
|
|
129
|
+
return { session, done };
|
|
130
|
+
}
|
|
131
|
+
session.process = child;
|
|
132
|
+
const handleData = (data) => {
|
|
133
|
+
const raw = data.toString("utf-8");
|
|
134
|
+
const clean = stripAnsi(raw);
|
|
135
|
+
session.output += clean;
|
|
136
|
+
if (session.output.length > maxOutput) {
|
|
137
|
+
session.output = session.output.slice(-maxOutput);
|
|
138
|
+
session.truncated = true;
|
|
139
|
+
}
|
|
140
|
+
opts.onOutput?.(raw);
|
|
141
|
+
};
|
|
142
|
+
child.stdout?.on("data", handleData);
|
|
143
|
+
child.stderr?.on("data", handleData);
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
if (!session.done && session.process) {
|
|
146
|
+
try {
|
|
147
|
+
session.process.kill("SIGTERM");
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
if (!session.done && session.process) {
|
|
152
|
+
try {
|
|
153
|
+
session.process.kill("SIGKILL");
|
|
154
|
+
}
|
|
155
|
+
catch { }
|
|
156
|
+
}
|
|
157
|
+
}, 5000).unref();
|
|
158
|
+
}
|
|
159
|
+
}, timeout);
|
|
160
|
+
child.on("exit", (code, signal) => {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
session.exitCode = code ?? (signal ? -1 : null);
|
|
163
|
+
session.done = true;
|
|
164
|
+
session.process = null;
|
|
165
|
+
session.resolve?.();
|
|
166
|
+
});
|
|
167
|
+
child.on("error", (err) => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
if (!session.done) {
|
|
170
|
+
session.exitCode = -1;
|
|
171
|
+
session.output += `\nProcess error: ${err.message}`;
|
|
172
|
+
session.done = true;
|
|
173
|
+
session.process = null;
|
|
174
|
+
session.resolve?.();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return { session, done };
|
|
178
|
+
}
|
|
90
179
|
/**
|
|
91
180
|
* Kill a running session's process group: SIGTERM, then SIGKILL after 5s.
|
|
92
181
|
* Returns a cleanup that cancels the pending SIGKILL — callers should invoke
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getSettings } from "../settings.js";
|
|
2
2
|
const BASE_URL = "https://openrouter.ai/api/v1";
|
|
3
|
-
const DEFAULT_MODELS = ["
|
|
3
|
+
const DEFAULT_MODELS = ["deepseek/deepseek-v4-flash"];
|
|
4
4
|
// Built-in defaults for models requiring reasoning_content echoed back
|
|
5
5
|
// (server 400s without it). Extend or override in settings.json:
|
|
6
6
|
// providers.openrouter.echoReasoningPatterns = ["deepseek", "..."]
|
package/dist/shell/shell.js
CHANGED
|
@@ -97,6 +97,7 @@ export class Shell {
|
|
|
97
97
|
...(showIndicator ? [` ${titleCmd}`] : []),
|
|
98
98
|
" __agent_sh_preexec_ran=0",
|
|
99
99
|
"}",
|
|
100
|
+
`PROMPT_COMMAND="\${PROMPT_COMMAND%;}"`,
|
|
100
101
|
`PROMPT_COMMAND="\${PROMPT_COMMAND:+\$PROMPT_COMMAND;}__agent_sh_precmd"`,
|
|
101
102
|
"",
|
|
102
103
|
"# Preexec hook via DEBUG trap: emit actual command text so agent-sh",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the ripgrep binary path. Prefers the version bundled via
|
|
3
|
+
* @vscode/ripgrep (downloaded by its postinstall hook). Falls back to plain
|
|
4
|
+
* "rg" so users with rg on PATH still work even if the postinstall failed
|
|
5
|
+
* (offline install, blocked egress, etc.).
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveRgPath(): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { rgPath as bundledRgPath } from "@vscode/ripgrep";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the ripgrep binary path. Prefers the version bundled via
|
|
5
|
+
* @vscode/ripgrep (downloaded by its postinstall hook). Falls back to plain
|
|
6
|
+
* "rg" so users with rg on PATH still work even if the postinstall failed
|
|
7
|
+
* (offline install, blocked egress, etc.).
|
|
8
|
+
*/
|
|
9
|
+
export function resolveRgPath() {
|
|
10
|
+
try {
|
|
11
|
+
if (bundledRgPath && fs.existsSync(bundledRgPath))
|
|
12
|
+
return bundledRgPath;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// fall through
|
|
16
|
+
}
|
|
17
|
+
return "rg";
|
|
18
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
1
2
|
import { visibleLen } from "./ansi.js";
|
|
2
3
|
import { palette as p } from "./palette.js";
|
|
3
4
|
// ── Quiet command detection ──────────────────────────────────────
|
|
@@ -231,7 +232,7 @@ function shortenPath(p, cwd) {
|
|
|
231
232
|
return p.slice(cwd.length + 1);
|
|
232
233
|
if (p.startsWith(cwd))
|
|
233
234
|
return p.slice(cwd.length) || ".";
|
|
234
|
-
const home = process.env.HOME;
|
|
235
|
+
const home = process.env.HOME ?? os.homedir();
|
|
235
236
|
if (home && p.startsWith(home + "/"))
|
|
236
237
|
return "~/" + p.slice(home.length + 1);
|
|
237
238
|
return p;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.8",
|
|
4
4
|
"description": "A shell-first terminal where AI is one keystroke away",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/core.js",
|
|
@@ -127,6 +127,7 @@
|
|
|
127
127
|
"node": ">=18"
|
|
128
128
|
},
|
|
129
129
|
"dependencies": {
|
|
130
|
+
"@vscode/ripgrep": "^1.17.1",
|
|
130
131
|
"@xterm/addon-serialize": "^0.13.0",
|
|
131
132
|
"@xterm/headless": "^5.5.0",
|
|
132
133
|
"cli-highlight": "^2.1.11",
|