agent-sh 0.15.4 → 0.15.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.
@@ -1,8 +1,48 @@
1
1
  import { spawn } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
2
4
  import { pickStrategy, FALLBACK_STRATEGY } from "../shell/strategies/index.js";
5
+ import { CONFIG_DIR } from "../core/settings.js";
6
+ const CACHE_FILE = path.join(CONFIG_DIR, "cache", "shell-env.json");
7
+ function captureSignature(shell, strategy, captureCmd) {
8
+ const files = strategy.envCaptureFiles?.(process.env) ?? [];
9
+ const stamps = files.sort().map((f) => {
10
+ try {
11
+ return [f, fs.statSync(f).mtimeMs];
12
+ }
13
+ catch {
14
+ return [f, 0];
15
+ }
16
+ });
17
+ return JSON.stringify({ shell, captureCmd, stamps });
18
+ }
19
+ function readCachedEnv(sig) {
20
+ try {
21
+ const raw = JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
22
+ if (raw?.sig === sig && raw.env && typeof raw.env === "object")
23
+ return raw.env;
24
+ }
25
+ catch { }
26
+ return null;
27
+ }
28
+ function writeCachedEnv(sig, env) {
29
+ try {
30
+ fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
31
+ fs.writeFileSync(CACHE_FILE, JSON.stringify({ sig, env }));
32
+ }
33
+ catch { }
34
+ }
3
35
  export async function captureShellEnvAsync(shell) {
4
36
  if (process.env.AGENT_SH_SKIP_SHELL_ENV)
5
37
  return {};
38
+ const strategy = pickStrategy(shell) ?? FALLBACK_STRATEGY;
39
+ const captureCmd = strategy.envCaptureCommand();
40
+ const sig = captureSignature(shell, strategy, captureCmd);
41
+ if (!process.env.AGENT_SH_SHELL_ENV_NOCACHE) {
42
+ const cached = readCachedEnv(sig);
43
+ if (cached)
44
+ return cached;
45
+ }
6
46
  return new Promise((resolve) => {
7
47
  let settled = false;
8
48
  const done = (result) => {
@@ -12,8 +52,6 @@ export async function captureShellEnvAsync(shell) {
12
52
  resolve(result);
13
53
  };
14
54
  try {
15
- const strategy = pickStrategy(shell) ?? FALLBACK_STRATEGY;
16
- const captureCmd = strategy.envCaptureCommand();
17
55
  const child = spawn(shell, ["-l", "-c", captureCmd], {
18
56
  stdio: ["ignore", "pipe", "ignore"],
19
57
  timeout: 5000,
@@ -34,6 +72,7 @@ export async function captureShellEnvAsync(shell) {
34
72
  if (eq > 0)
35
73
  env[entry.slice(0, eq)] = entry.slice(eq + 1);
36
74
  }
75
+ writeCachedEnv(sig, env);
37
76
  done(env);
38
77
  });
39
78
  child.on("error", () => {
@@ -66,6 +66,12 @@ export const bashStrategy = {
66
66
  envCaptureCommand() {
67
67
  return "[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null; env -0";
68
68
  },
69
+ envCaptureFiles(env) {
70
+ const home = env.HOME;
71
+ if (!home)
72
+ return [];
73
+ return [".bashrc", ".bash_profile", ".bash_login", ".profile"].map((f) => path.join(home, f));
74
+ },
69
75
  redrawEscape() {
70
76
  return "\x1b[9999~";
71
77
  },
@@ -59,6 +59,12 @@ export const fishStrategy = {
59
59
  // `fish -l` already sources config.fish + conf.d, so no explicit source.
60
60
  return "env -0";
61
61
  },
62
+ envCaptureFiles(env) {
63
+ const config = env.XDG_CONFIG_HOME || (env.HOME ? path.join(env.HOME, ".config") : undefined);
64
+ if (!config)
65
+ return [];
66
+ return [path.join(config, "fish", "config.fish"), path.join(config, "fish", "conf.d")];
67
+ },
62
68
  redrawEscape() {
63
69
  return "\x1b[57400u";
64
70
  },
@@ -41,6 +41,7 @@ export interface ShellStrategy {
41
41
  * config and dump env. Used at startup to inherit shell-only env vars.
42
42
  */
43
43
  envCaptureCommand(): string;
44
+ envCaptureFiles?(env: Record<string, string | undefined>): string[];
44
45
  /**
45
46
  * Escape sequence to write to the PTY to ask the shell to repaint its
46
47
  * prompt in place. The corresponding binding is set up in prepareSpawn.
@@ -66,6 +66,12 @@ export const zshStrategy = {
66
66
  envCaptureCommand() {
67
67
  return "source ~/.zshrc 2>/dev/null; env -0";
68
68
  },
69
+ envCaptureFiles(env) {
70
+ const zdot = env.ZDOTDIR || env.HOME;
71
+ if (!zdot)
72
+ return [];
73
+ return [".zshenv", ".zprofile", ".zshrc", ".zlogin"].map((f) => path.join(zdot, f));
74
+ },
69
75
  redrawEscape() {
70
76
  return "\x1b[9999~";
71
77
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.15.4",
3
+ "version": "0.15.5",
4
4
  "description": "A composable agent runtime — pair any frontend with any agent backend over one shared extension layer",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,8 +1,50 @@
1
1
  import { spawn } from "node:child_process";
2
- import { pickStrategy, FALLBACK_STRATEGY } from "../shell/strategies/index.js";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { pickStrategy, FALLBACK_STRATEGY, type ShellStrategy } from "../shell/strategies/index.js";
5
+ import { CONFIG_DIR } from "../core/settings.js";
6
+
7
+ const CACHE_FILE = path.join(CONFIG_DIR, "cache", "shell-env.json");
8
+
9
+ function captureSignature(shell: string, strategy: ShellStrategy, captureCmd: string): string {
10
+ const files = strategy.envCaptureFiles?.(process.env) ?? [];
11
+ const stamps = files.sort().map((f) => {
12
+ try {
13
+ return [f, fs.statSync(f).mtimeMs];
14
+ } catch {
15
+ return [f, 0];
16
+ }
17
+ });
18
+ return JSON.stringify({ shell, captureCmd, stamps });
19
+ }
20
+
21
+ function readCachedEnv(sig: string): Record<string, string> | null {
22
+ try {
23
+ const raw = JSON.parse(fs.readFileSync(CACHE_FILE, "utf-8"));
24
+ if (raw?.sig === sig && raw.env && typeof raw.env === "object") return raw.env;
25
+ } catch {}
26
+ return null;
27
+ }
28
+
29
+ function writeCachedEnv(sig: string, env: Record<string, string>): void {
30
+ try {
31
+ fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
32
+ fs.writeFileSync(CACHE_FILE, JSON.stringify({ sig, env }));
33
+ } catch {}
34
+ }
3
35
 
4
36
  export async function captureShellEnvAsync(shell: string): Promise<Record<string, string>> {
5
37
  if (process.env.AGENT_SH_SKIP_SHELL_ENV) return {};
38
+
39
+ const strategy = pickStrategy(shell) ?? FALLBACK_STRATEGY;
40
+ const captureCmd = strategy.envCaptureCommand();
41
+ const sig = captureSignature(shell, strategy, captureCmd);
42
+
43
+ if (!process.env.AGENT_SH_SHELL_ENV_NOCACHE) {
44
+ const cached = readCachedEnv(sig);
45
+ if (cached) return cached;
46
+ }
47
+
6
48
  return new Promise((resolve) => {
7
49
  let settled = false;
8
50
  const done = (result: Record<string, string>): void => {
@@ -12,9 +54,6 @@ export async function captureShellEnvAsync(shell: string): Promise<Record<string
12
54
  };
13
55
 
14
56
  try {
15
- const strategy = pickStrategy(shell) ?? FALLBACK_STRATEGY;
16
- const captureCmd = strategy.envCaptureCommand();
17
-
18
57
  const child = spawn(shell, ["-l", "-c", captureCmd], {
19
58
  stdio: ["ignore", "pipe", "ignore"],
20
59
  timeout: 5000,
@@ -36,6 +75,7 @@ export async function captureShellEnvAsync(shell: string): Promise<Record<string
36
75
  const eq = entry.indexOf("=");
37
76
  if (eq > 0) env[entry.slice(0, eq)] = entry.slice(eq + 1);
38
77
  }
78
+ writeCachedEnv(sig, env);
39
79
  done(env);
40
80
  });
41
81
 
@@ -77,6 +77,12 @@ export const bashStrategy: ShellStrategy = {
77
77
  return "[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null; env -0";
78
78
  },
79
79
 
80
+ envCaptureFiles(env): string[] {
81
+ const home = env.HOME;
82
+ if (!home) return [];
83
+ return [".bashrc", ".bash_profile", ".bash_login", ".profile"].map((f) => path.join(home, f));
84
+ },
85
+
80
86
  redrawEscape(): string {
81
87
  return "\x1b[9999~";
82
88
  },
@@ -71,6 +71,12 @@ export const fishStrategy: ShellStrategy = {
71
71
  return "env -0";
72
72
  },
73
73
 
74
+ envCaptureFiles(env): string[] {
75
+ const config = env.XDG_CONFIG_HOME || (env.HOME ? path.join(env.HOME, ".config") : undefined);
76
+ if (!config) return [];
77
+ return [path.join(config, "fish", "config.fish"), path.join(config, "fish", "conf.d")];
78
+ },
79
+
74
80
  redrawEscape(): string {
75
81
  return "\x1b[57400u";
76
82
  },
@@ -48,6 +48,8 @@ export interface ShellStrategy {
48
48
  */
49
49
  envCaptureCommand(): string;
50
50
 
51
+ envCaptureFiles?(env: Record<string, string | undefined>): string[];
52
+
51
53
  /**
52
54
  * Escape sequence to write to the PTY to ask the shell to repaint its
53
55
  * prompt in place. The corresponding binding is set up in prepareSpawn.
@@ -77,6 +77,12 @@ export const zshStrategy: ShellStrategy = {
77
77
  return "source ~/.zshrc 2>/dev/null; env -0";
78
78
  },
79
79
 
80
+ envCaptureFiles(env): string[] {
81
+ const zdot = env.ZDOTDIR || env.HOME;
82
+ if (!zdot) return [];
83
+ return [".zshenv", ".zprofile", ".zshrc", ".zlogin"].map((f) => path.join(zdot, f));
84
+ },
85
+
80
86
  redrawEscape(): string {
81
87
  return "\x1b[9999~";
82
88
  },