agent-sh 0.13.6 → 0.13.7

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.
@@ -0,0 +1,2 @@
1
+ import type { AppConfig } from "../shell/host-types.js";
2
+ export declare function parseArgs(argv: string[], env?: NodeJS.ProcessEnv): AppConfig;
@@ -0,0 +1,90 @@
1
+ import { PACKAGE_VERSION } from "../utils/package-version.js";
2
+ const HELP_TEXT = `agent-sh — a shell-first terminal where AI is one keystroke away
3
+
4
+ Usage: agent-sh [options]
5
+ agent-sh init [--force] Scaffold ~/.agent-sh/ (settings, examples, AGENTS.md)
6
+ agent-sh install <spec> [--force] Install an extension (bundled name, file:, npm:, github:)
7
+ agent-sh uninstall <name> Remove an installed extension
8
+ agent-sh list List installed extensions
9
+ agent-sh auth login [provider] Store an API key for a built-in provider
10
+ agent-sh auth logout <provider> Remove a stored key
11
+ agent-sh auth list Show configured providers
12
+
13
+ Provider Profiles:
14
+ --provider <name> Use a provider from ~/.agent-sh/settings.json
15
+ --model <name> Override default model
16
+
17
+ Direct LLM API:
18
+ --api-key <key> API key for OpenAI-compatible provider (or set OPENAI_API_KEY)
19
+ --base-url <url> Base URL for API (or set OPENAI_BASE_URL)
20
+
21
+ General Options:
22
+ --backend <name> Agent backend to launch (e.g. ash, pi); overrides settings.defaultBackend for this session
23
+ --shell <path> Shell to use (default: $SHELL or /bin/bash)
24
+ -e, --extensions Extensions to load (comma-separated, repeatable)
25
+ -h, --help Show this help
26
+ -V, --version Print version and exit
27
+
28
+ Environment Variables:
29
+ OPENAI_API_KEY API key for LLM provider
30
+ OPENAI_BASE_URL Base URL override (e.g., http://localhost:11434/v1 for Ollama)
31
+
32
+ Examples:
33
+ # Use a configured provider
34
+ agent-sh --provider openai
35
+
36
+ # Direct API access
37
+ agent-sh --api-key "$KEY" --model gpt-4o
38
+
39
+ # Local model via Ollama
40
+ agent-sh --base-url http://localhost:11434/v1 --model llama3
41
+
42
+ Inside the shell:
43
+ Type normally Commands run in your real shell
44
+ > <query> Ask the AI agent (it decides how to help)
45
+ > /help Show available slash commands
46
+ Ctrl-C Cancel agent response (or signal shell as usual)
47
+ `;
48
+ export function parseArgs(argv, env = process.env) {
49
+ let model;
50
+ let extensions;
51
+ let provider;
52
+ let backend;
53
+ let shell = env.SHELL || "/bin/bash";
54
+ let apiKey = env.OPENAI_API_KEY;
55
+ let baseURL = env.OPENAI_BASE_URL;
56
+ for (let i = 0; i < argv.length; i++) {
57
+ const arg = argv[i];
58
+ if (arg === "--model" && argv[i + 1]) {
59
+ model = argv[++i];
60
+ }
61
+ else if (arg === "--api-key" && argv[i + 1]) {
62
+ apiKey = argv[++i];
63
+ }
64
+ else if (arg === "--base-url" && argv[i + 1]) {
65
+ baseURL = argv[++i];
66
+ }
67
+ else if (arg === "--provider" && argv[i + 1]) {
68
+ provider = argv[++i];
69
+ }
70
+ else if (arg === "--backend" && argv[i + 1]) {
71
+ backend = argv[++i];
72
+ }
73
+ else if (arg === "--shell" && argv[i + 1]) {
74
+ shell = argv[++i];
75
+ }
76
+ else if ((arg === "--extensions" || arg === "-e") && argv[i + 1]) {
77
+ const exts = argv[++i].split(",").map((s) => s.trim());
78
+ extensions = extensions ? [...extensions, ...exts] : exts;
79
+ }
80
+ else if (arg === "--version" || arg === "-V") {
81
+ console.log(PACKAGE_VERSION);
82
+ process.exit(0);
83
+ }
84
+ else if (arg === "--help" || arg === "-h") {
85
+ console.log(HELP_TEXT);
86
+ process.exit(0);
87
+ }
88
+ }
89
+ return { shell, model, extensions, apiKey, baseURL, provider, backend };
90
+ }
package/dist/cli/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
2
  import { activateShell, registerShellHandlers } from "../shell/index.js";
4
- import { pickStrategy, FALLBACK_STRATEGY } from "../shell/strategies/index.js";
5
3
  import { activateAgent } from "../agent/index.js";
6
4
  import { createCore } from "../core/index.js";
7
5
  import { palette as p } from "../utils/palette.js";
@@ -11,158 +9,9 @@ import { getSettings } from "../core/settings.js";
11
9
  import { dispatchSubcommand } from "./subcommands.js";
12
10
  import { suggestBridgeFor } from "./install.js";
13
11
  import { anyProviderConfigured } from "./auth/keys.js";
14
- import { PACKAGE_VERSION } from "../utils/package-version.js";
15
12
  import { clearOpost } from "../utils/tty.js";
16
- /**
17
- * Capture the user's full shell environment.
18
- * This picks up env vars exported in .zshrc/.bashrc that the
19
- * Node.js process doesn't have (e.g. when launched from an IDE).
20
- */
21
- async function captureShellEnvAsync(shell) {
22
- return new Promise((resolve) => {
23
- let settled = false;
24
- const done = (result) => {
25
- if (settled)
26
- return;
27
- settled = true;
28
- resolve(result);
29
- };
30
- try {
31
- const strategy = pickStrategy(shell) ?? FALLBACK_STRATEGY;
32
- const captureCmd = strategy.envCaptureCommand();
33
- const child = spawn(shell, ["-l", "-c", captureCmd], {
34
- stdio: ["ignore", "pipe", "ignore"],
35
- timeout: 5000,
36
- });
37
- let output = "";
38
- child.stdout?.on("data", (data) => {
39
- output += data.toString("utf-8");
40
- });
41
- child.on("close", (code) => {
42
- clearTimeout(timer);
43
- if (code !== 0 || !output) {
44
- done({});
45
- return;
46
- }
47
- const env = {};
48
- for (const entry of output.split("\0")) {
49
- const eq = entry.indexOf("=");
50
- if (eq > 0)
51
- env[entry.slice(0, eq)] = entry.slice(eq + 1);
52
- }
53
- done(env);
54
- });
55
- child.on("error", () => {
56
- clearTimeout(timer);
57
- done({});
58
- });
59
- const timer = setTimeout(() => {
60
- child.kill("SIGTERM");
61
- done({});
62
- }, 5000);
63
- }
64
- catch {
65
- done({});
66
- }
67
- });
68
- }
69
- function mergeShellEnv(baseEnv, shellEnv) {
70
- const merged = { ...baseEnv };
71
- for (const [key, value] of Object.entries(shellEnv)) {
72
- if (!(key in merged) || !merged[key]) {
73
- merged[key] = value;
74
- }
75
- }
76
- return merged;
77
- }
78
- function parseArgs(argv) {
79
- let model;
80
- let extensions;
81
- let provider;
82
- let backend;
83
- let shell = process.env.SHELL || "/bin/bash";
84
- let apiKey = process.env.OPENAI_API_KEY;
85
- let baseURL = process.env.OPENAI_BASE_URL;
86
- for (let i = 0; i < argv.length; i++) {
87
- const arg = argv[i];
88
- if (arg === "--model" && argv[i + 1]) {
89
- model = argv[++i];
90
- }
91
- else if (arg === "--api-key" && argv[i + 1]) {
92
- apiKey = argv[++i];
93
- }
94
- else if (arg === "--base-url" && argv[i + 1]) {
95
- baseURL = argv[++i];
96
- }
97
- else if (arg === "--provider" && argv[i + 1]) {
98
- provider = argv[++i];
99
- }
100
- else if (arg === "--backend" && argv[i + 1]) {
101
- backend = argv[++i];
102
- }
103
- else if (arg === "--shell" && argv[i + 1]) {
104
- shell = argv[++i];
105
- }
106
- else if ((arg === "--extensions" || arg === "-e") && argv[i + 1]) {
107
- const exts = argv[++i].split(",").map(s => s.trim());
108
- extensions = extensions ? [...extensions, ...exts] : exts;
109
- }
110
- else if (arg === "--version" || arg === "-V") {
111
- console.log(PACKAGE_VERSION);
112
- process.exit(0);
113
- }
114
- else if (arg === "--help" || arg === "-h") {
115
- console.log(`agent-sh — a shell-first terminal where AI is one keystroke away
116
-
117
- Usage: agent-sh [options]
118
- agent-sh init [--force] Scaffold ~/.agent-sh/ (settings, examples, AGENTS.md)
119
- agent-sh install <spec> [--force] Install an extension (bundled name, file:, npm:, github:)
120
- agent-sh uninstall <name> Remove an installed extension
121
- agent-sh list List installed extensions
122
- agent-sh auth login [provider] Store an API key for a built-in provider
123
- agent-sh auth logout <provider> Remove a stored key
124
- agent-sh auth list Show configured providers
125
-
126
- Provider Profiles:
127
- --provider <name> Use a provider from ~/.agent-sh/settings.json
128
- --model <name> Override default model
129
-
130
- Direct LLM API:
131
- --api-key <key> API key for OpenAI-compatible provider (or set OPENAI_API_KEY)
132
- --base-url <url> Base URL for API (or set OPENAI_BASE_URL)
133
-
134
- General Options:
135
- --backend <name> Agent backend to launch (e.g. ash, pi); overrides settings.defaultBackend for this session
136
- --shell <path> Shell to use (default: $SHELL or /bin/bash)
137
- -e, --extensions Extensions to load (comma-separated, repeatable)
138
- -h, --help Show this help
139
- -V, --version Print version and exit
140
-
141
- Environment Variables:
142
- OPENAI_API_KEY API key for LLM provider
143
- OPENAI_BASE_URL Base URL override (e.g., http://localhost:11434/v1 for Ollama)
144
-
145
- Examples:
146
- # Use a configured provider
147
- agent-sh --provider openai
148
-
149
- # Direct API access
150
- agent-sh --api-key "$KEY" --model gpt-4o
151
-
152
- # Local model via Ollama
153
- agent-sh --base-url http://localhost:11434/v1 --model llama3
154
-
155
- Inside the shell:
156
- Type normally Commands run in your real shell
157
- > <query> Ask the AI agent (it decides how to help)
158
- > /help Show available slash commands
159
- Ctrl-C Cancel agent response (or signal shell as usual)
160
- `);
161
- process.exit(0);
162
- }
163
- }
164
- return { shell, model, extensions, apiKey, baseURL, provider, backend };
165
- }
13
+ import { parseArgs } from "./args.js";
14
+ import { captureShellEnvAsync, mergeShellEnv } from "./shell-env.js";
166
15
  async function main() {
167
16
  const rawArgs = process.argv.slice(2);
168
17
  if (await dispatchSubcommand(rawArgs))
@@ -293,6 +142,7 @@ async function main() {
293
142
  "\n " + hint + "\n" +
294
143
  borderLine + "\n\n");
295
144
  }
145
+ await core.activateBackend(config.backend);
296
146
  // 100ms sidesteps macOS SIGTTOU during fg-pgrp handoff.
297
147
  await new Promise(resolve => setTimeout(resolve, 100));
298
148
  shell = activateShell(extCtx, {
@@ -318,7 +168,6 @@ async function main() {
318
168
  },
319
169
  returnToSelf: true,
320
170
  });
321
- core.activateBackend(config.backend);
322
171
  // ── Terminal lifecycle ────────────────────────────────────────
323
172
  process.on("SIGTERM", cleanup);
324
173
  process.on("SIGHUP", cleanup);
@@ -0,0 +1,2 @@
1
+ export declare function captureShellEnvAsync(shell: string): Promise<Record<string, string>>;
2
+ export declare function mergeShellEnv(baseEnv: Record<string, string>, shellEnv: Record<string, string>): Record<string, string>;
@@ -0,0 +1,61 @@
1
+ import { spawn } from "node:child_process";
2
+ import { pickStrategy, FALLBACK_STRATEGY } from "../shell/strategies/index.js";
3
+ export async function captureShellEnvAsync(shell) {
4
+ if (process.env.AGENT_SH_SKIP_SHELL_ENV)
5
+ return {};
6
+ return new Promise((resolve) => {
7
+ let settled = false;
8
+ const done = (result) => {
9
+ if (settled)
10
+ return;
11
+ settled = true;
12
+ resolve(result);
13
+ };
14
+ try {
15
+ const strategy = pickStrategy(shell) ?? FALLBACK_STRATEGY;
16
+ const captureCmd = strategy.envCaptureCommand();
17
+ const child = spawn(shell, ["-l", "-c", captureCmd], {
18
+ stdio: ["ignore", "pipe", "ignore"],
19
+ timeout: 5000,
20
+ });
21
+ let output = "";
22
+ child.stdout?.on("data", (data) => {
23
+ output += data.toString("utf-8");
24
+ });
25
+ child.on("close", (code) => {
26
+ clearTimeout(timer);
27
+ if (code !== 0 || !output) {
28
+ done({});
29
+ return;
30
+ }
31
+ const env = {};
32
+ for (const entry of output.split("\0")) {
33
+ const eq = entry.indexOf("=");
34
+ if (eq > 0)
35
+ env[entry.slice(0, eq)] = entry.slice(eq + 1);
36
+ }
37
+ done(env);
38
+ });
39
+ child.on("error", () => {
40
+ clearTimeout(timer);
41
+ done({});
42
+ });
43
+ const timer = setTimeout(() => {
44
+ child.kill("SIGTERM");
45
+ done({});
46
+ }, 5000);
47
+ }
48
+ catch {
49
+ done({});
50
+ }
51
+ });
52
+ }
53
+ export function mergeShellEnv(baseEnv, shellEnv) {
54
+ const merged = { ...baseEnv };
55
+ for (const [key, value] of Object.entries(shellEnv)) {
56
+ if (!(key in merged) || !merged[key]) {
57
+ merged[key] = value;
58
+ }
59
+ }
60
+ return merged;
61
+ }
@@ -56,8 +56,9 @@ export function createCore(config) {
56
56
  bus.emit("ui:error", { message: `Unknown backend: ${name}` });
57
57
  return false;
58
58
  }
59
- if (activeBackendName) {
60
- backends.get(activeBackendName)?.kill();
59
+ for (const [otherName, otherBackend] of backends) {
60
+ if (otherName !== name)
61
+ otherBackend.kill();
61
62
  }
62
63
  await backend.start?.();
63
64
  activeBackendName = name;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.13.6",
3
+ "version": "0.13.7",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core/index.js",
@@ -121,6 +121,7 @@
121
121
  "dev": "tsx src/cli/index.ts",
122
122
  "build": "tsc",
123
123
  "start": "node dist/cli/index.js",
124
+ "test": "npm run build && node --import tsx --test $(find tests -name '*.test.ts' -type f)",
124
125
  "prepare": "test -d dist || npm run build"
125
126
  },
126
127
  "keywords": [