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 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.
@@ -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);
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { executeCommand } from "../../executor.js";
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 shellEsc = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
46
- const parts = [
47
- "rg", "--files",
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.",
@@ -1,4 +1,5 @@
1
- import { executeCommand } from "../../executor.js";
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 shellEsc = (s) => "'" + s.replace(/'/g, "'\\''") + "'";
93
- const parts = ["rg", "--color=never"];
93
+ const rgArgs = ["--color=never"];
94
94
  // Mode-specific flags
95
95
  if (mode === "files_with_matches") {
96
- parts.push("--files-with-matches");
96
+ rgArgs.push("--files-with-matches");
97
97
  }
98
98
  else if (mode === "count") {
99
- parts.push("--count");
99
+ rgArgs.push("--count");
100
100
  }
101
101
  else {
102
102
  // content mode
103
- parts.push("--line-number", "--no-heading");
103
+ rgArgs.push("--line-number", "--no-heading");
104
104
  if (contextBefore != null && contextBefore > 0) {
105
- parts.push(`-B${contextBefore}`);
105
+ rgArgs.push(`-B${contextBefore}`);
106
106
  }
107
107
  if (contextAfter != null && contextAfter > 0) {
108
- parts.push(`-A${contextAfter}`);
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
- parts.push("--max-count=50");
112
+ rgArgs.push("--max-count=50");
113
113
  }
114
114
  }
115
115
  if (caseInsensitive) {
116
- parts.push("-i");
116
+ rgArgs.push("-i");
117
117
  }
118
118
  if (include) {
119
- parts.push("--glob", shellEsc(include));
119
+ rgArgs.push("--glob", include);
120
120
  }
121
- parts.push("-e", shellEsc(pattern), shellEsc(searchPath));
122
- const { session, done } = executeCommand({
123
- command: parts.join(" "),
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.
@@ -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 = ["anthropic/claude-sonnet-4.6"];
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", "..."]
@@ -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.6",
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",