agent-sh 0.13.6 → 0.14.0
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 +1 -1
- package/dist/agent/agent-loop.d.ts +13 -17
- package/dist/agent/agent-loop.js +118 -224
- package/dist/agent/conversation-state.d.ts +1 -1
- package/dist/agent/events.d.ts +218 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/host-types.d.ts +20 -0
- package/dist/agent/index.d.ts +5 -9
- package/dist/agent/index.js +269 -167
- package/dist/agent/llm-facade.d.ts +13 -0
- package/dist/{utils → agent}/llm-facade.js +1 -1
- package/dist/agent/nuclear-form.d.ts +1 -1
- package/dist/agent/providers/deepseek.js +2 -5
- package/dist/agent/providers/openai-compatible.js +2 -2
- package/dist/agent/providers/openai.js +2 -5
- package/dist/agent/providers/openrouter.js +5 -5
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-registry.d.ts +1 -1
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +90 -0
- package/dist/cli/auth/cli.js +11 -6
- package/dist/cli/auth/discover.d.ts +5 -0
- package/dist/cli/auth/discover.js +25 -0
- package/dist/cli/auth/keys.d.ts +5 -2
- package/dist/cli/auth/keys.js +22 -2
- package/dist/cli/index.d.ts +16 -0
- package/dist/cli/index.js +15 -156
- package/dist/cli/shell-env.d.ts +2 -0
- package/dist/cli/shell-env.js +61 -0
- package/dist/core/event-bus.d.ts +28 -371
- package/dist/core/extension-loader.js +6 -6
- package/dist/core/index.d.ts +10 -29
- package/dist/core/index.js +31 -82
- package/dist/extensions/index.d.ts +2 -1
- package/dist/extensions/index.js +1 -1
- package/dist/extensions/slash-commands/events.d.ts +18 -0
- package/dist/extensions/slash-commands/events.js +1 -0
- package/dist/extensions/slash-commands/index.d.ts +15 -0
- package/dist/extensions/{slash-commands.js → slash-commands/index.js} +4 -3
- package/dist/shell/events.d.ts +85 -0
- package/dist/shell/events.js +1 -0
- package/dist/shell/index.d.ts +1 -0
- package/dist/shell/index.js +6 -0
- package/dist/shell/tui-renderer.js +0 -1
- package/examples/extensions/ash-acp-bridge/src/index.ts +2 -2
- package/examples/extensions/ashi/package.json +1 -1
- package/examples/extensions/ollama.ts +47 -42
- package/examples/extensions/opencode-bridge/README.md +4 -0
- package/examples/extensions/opencode-bridge/index.ts +3 -1
- package/examples/extensions/pi-bridge/index.ts +3 -4
- package/examples/extensions/zai-coding-plan.ts +2 -6
- package/package.json +2 -1
- package/dist/extensions/slash-commands.d.ts +0 -2
- package/dist/utils/llm-facade.d.ts +0 -11
- /package/dist/{utils → agent}/llm-client.d.ts +0 -0
- /package/dist/{utils → agent}/llm-client.js +0 -0
package/dist/agent/subagent.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Used by the subagent extension to delegate tasks from the main agent.
|
|
11
11
|
*/
|
|
12
12
|
import type { EventBus } from "../core/event-bus.js";
|
|
13
|
-
import type { LlmClient } from "
|
|
13
|
+
import type { LlmClient } from "./llm-client.js";
|
|
14
14
|
import type { ToolDefinition } from "./types.js";
|
|
15
15
|
export interface SubagentOptions {
|
|
16
16
|
/** LLM client to use. */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* The agent loop uses this interface uniformly so the rest of the code
|
|
10
10
|
* doesn't need to know which mode is active.
|
|
11
11
|
*/
|
|
12
|
-
import type { ChatCompletionTool } from "
|
|
12
|
+
import type { ChatCompletionTool } from "./llm-client.js";
|
|
13
13
|
import type { ToolDefinition } from "./types.js";
|
|
14
14
|
import type { ConversationState } from "./conversation-state.js";
|
|
15
15
|
export interface PendingToolCall {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolDefinition, ToolResult } from "./types.js";
|
|
2
|
-
import type { ChatCompletionTool } from "
|
|
2
|
+
import type { ChatCompletionTool } from "./llm-client.js";
|
|
3
3
|
import type { HandlerFunctions } from "../utils/handler-registry.js";
|
|
4
4
|
/**
|
|
5
5
|
* Registry for agent tools. Execution is routed through the named-handler
|
package/dist/cli/args.js
ADDED
|
@@ -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/auth/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
2
|
import { palette as p } from "../../utils/palette.js";
|
|
3
|
-
import { KNOWN_PROVIDERS, KEYS_PATH, loadKeysFile, saveKeysFile, resolveApiKey,
|
|
3
|
+
import { KNOWN_PROVIDERS, KEYS_PATH, loadKeysFile, saveKeysFile, resolveApiKey, listAllProvidersWithDiscovery, findProvider as findProviderById, } from "./keys.js";
|
|
4
4
|
export async function runAuth(args) {
|
|
5
5
|
const sub = args[0];
|
|
6
6
|
if (!sub || sub === "--help" || sub === "-h") {
|
|
@@ -16,7 +16,7 @@ export async function runAuth(args) {
|
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
if (sub === "list" || sub === "ls" || sub === "status") {
|
|
19
|
-
runList();
|
|
19
|
+
await runList();
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
console.error(`agent-sh auth: unknown subcommand "${sub}"`);
|
|
@@ -90,8 +90,8 @@ function runLogout(providerArg) {
|
|
|
90
90
|
saveKeysFile(keys);
|
|
91
91
|
console.log(`${p.success}✓${p.reset} Removed ${id} key from ${KEYS_PATH}`);
|
|
92
92
|
}
|
|
93
|
-
function runList() {
|
|
94
|
-
const providers =
|
|
93
|
+
async function runList() {
|
|
94
|
+
const providers = await listAllProvidersWithDiscovery();
|
|
95
95
|
console.log("Provider key status:\n");
|
|
96
96
|
const idWidth = Math.max(...providers.map((p) => p.id.length));
|
|
97
97
|
for (const info of providers) {
|
|
@@ -105,6 +105,9 @@ function runList() {
|
|
|
105
105
|
if (resolved.key) {
|
|
106
106
|
console.log(` ${p.success}●${p.reset} ${padded} ${p.dim}(${sourceLabel(resolved.source, info)})${p.reset}${marker}`);
|
|
107
107
|
}
|
|
108
|
+
else if (info.noAuth) {
|
|
109
|
+
console.log(` ${p.success}●${p.reset} ${padded} ${p.dim}(no auth required)${p.reset}${marker}`);
|
|
110
|
+
}
|
|
108
111
|
else {
|
|
109
112
|
console.log(` ${p.muted}○${p.reset} ${padded} ${p.dim}(not configured)${p.reset}${marker}`);
|
|
110
113
|
}
|
|
@@ -117,13 +120,15 @@ async function pickProvider() {
|
|
|
117
120
|
console.error("agent-sh auth: no provider specified and stdin is not a TTY.");
|
|
118
121
|
return null;
|
|
119
122
|
}
|
|
120
|
-
const providers =
|
|
123
|
+
const providers = await listAllProvidersWithDiscovery();
|
|
121
124
|
console.log("Select a provider:");
|
|
122
125
|
providers.forEach((info, i) => {
|
|
123
126
|
const resolved = resolveApiKey(info.id);
|
|
124
127
|
const tag = resolved.key
|
|
125
128
|
? `${p.dim}(currently from ${sourceLabel(resolved.source, info)})${p.reset}`
|
|
126
|
-
:
|
|
129
|
+
: info.noAuth
|
|
130
|
+
? `${p.dim}(no auth required)${p.reset}`
|
|
131
|
+
: `${p.dim}(not configured)${p.reset}`;
|
|
127
132
|
const labelStr = info.custom
|
|
128
133
|
? `${p.dim}custom${p.reset}`
|
|
129
134
|
: info.unattached
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Bootstrap a throwaway core to enumerate provider ids extensions
|
|
2
|
+
* would register, so `auth list` shows ids the user hasn't keyed yet. */
|
|
3
|
+
import { createCore } from "../../core/index.js";
|
|
4
|
+
import { activateAgent } from "../../agent/index.js";
|
|
5
|
+
import { loadExtensions } from "../../core/extension-loader.js";
|
|
6
|
+
import { loadBuiltinExtensions } from "../../extensions/index.js";
|
|
7
|
+
import { getSettings } from "../../core/settings.js";
|
|
8
|
+
let cached = null;
|
|
9
|
+
export async function discoverExtensionProviders() {
|
|
10
|
+
if (cached)
|
|
11
|
+
return cached;
|
|
12
|
+
const core = createCore({});
|
|
13
|
+
try {
|
|
14
|
+
const ctx = core.extensionContext({ quit: () => { } });
|
|
15
|
+
activateAgent(ctx);
|
|
16
|
+
await loadBuiltinExtensions(ctx, getSettings().disabledBuiltins);
|
|
17
|
+
await loadExtensions(ctx).catch(() => { });
|
|
18
|
+
const { providers } = core.bus.emitPipe("agent:providers", { providers: [] });
|
|
19
|
+
cached = providers.map((p) => ({ id: p.id, noAuth: p.noAuth }));
|
|
20
|
+
return cached;
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
core.kill();
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/cli/auth/keys.d.ts
CHANGED
|
@@ -9,11 +9,14 @@ export interface ProviderAuthInfo {
|
|
|
9
9
|
/** True for ids only present in keys.json — likely owned by an extension
|
|
10
10
|
* that registers a provider at runtime. */
|
|
11
11
|
unattached?: boolean;
|
|
12
|
+
/** Auth UI shows "no auth required" instead of "not configured". */
|
|
13
|
+
noAuth?: boolean;
|
|
12
14
|
}
|
|
13
15
|
export declare const KNOWN_PROVIDERS: ProviderAuthInfo[];
|
|
14
|
-
/** Built-ins
|
|
15
|
-
* appear in keys.json (likely registered by an extension at runtime). */
|
|
16
|
+
/** Built-ins + settings + keys.json. Sync, no extension load. */
|
|
16
17
|
export declare function listAllProviders(): ProviderAuthInfo[];
|
|
18
|
+
/** Augments listAllProviders with extension-registered ids. */
|
|
19
|
+
export declare function listAllProvidersWithDiscovery(): Promise<ProviderAuthInfo[]>;
|
|
17
20
|
/** Resolve an id against known + settings entries only. Returns null for
|
|
18
21
|
* unattached or unknown ids — callers decide whether to accept them. */
|
|
19
22
|
export declare function findProvider(id: string): ProviderAuthInfo | null;
|
package/dist/cli/auth/keys.js
CHANGED
|
@@ -8,8 +8,7 @@ export const KNOWN_PROVIDERS = [
|
|
|
8
8
|
{ id: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY" },
|
|
9
9
|
{ id: "deepseek", label: "DeepSeek", envVar: "DEEPSEEK_API_KEY" },
|
|
10
10
|
];
|
|
11
|
-
/** Built-ins
|
|
12
|
-
* appear in keys.json (likely registered by an extension at runtime). */
|
|
11
|
+
/** Built-ins + settings + keys.json. Sync, no extension load. */
|
|
13
12
|
export function listAllProviders() {
|
|
14
13
|
const out = [...KNOWN_PROVIDERS];
|
|
15
14
|
const seen = new Set(out.map((p) => p.id));
|
|
@@ -28,6 +27,27 @@ export function listAllProviders() {
|
|
|
28
27
|
}
|
|
29
28
|
return out;
|
|
30
29
|
}
|
|
30
|
+
/** Augments listAllProviders with extension-registered ids. */
|
|
31
|
+
export async function listAllProvidersWithDiscovery() {
|
|
32
|
+
const out = listAllProviders();
|
|
33
|
+
const byId = new Map(out.map((p) => [p.id, p]));
|
|
34
|
+
const { discoverExtensionProviders } = await import("./discover.js");
|
|
35
|
+
try {
|
|
36
|
+
for (const d of await discoverExtensionProviders()) {
|
|
37
|
+
const existing = byId.get(d.id);
|
|
38
|
+
if (existing) {
|
|
39
|
+
if (d.noAuth && !existing.noAuth)
|
|
40
|
+
existing.noAuth = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const entry = { id: d.id, label: d.id, custom: true, noAuth: d.noAuth };
|
|
44
|
+
out.push(entry);
|
|
45
|
+
byId.set(d.id, entry);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
31
51
|
/** Resolve an id against known + settings entries only. Returns null for
|
|
32
52
|
* unattached or unknown ids — callers decide whether to accept them. */
|
|
33
53
|
export function findProvider(id) {
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
declare module "../core/event-bus.js" {
|
|
3
|
+
interface BusEvents {
|
|
4
|
+
/** Startup banner collection (sync pipe). Extensions contribute
|
|
5
|
+
* labeled item lists; the CLI renders them between the product
|
|
6
|
+
* name and the help hint. */
|
|
7
|
+
"banner:collect": {
|
|
8
|
+
sections: Array<{
|
|
9
|
+
label: string;
|
|
10
|
+
items: string[];
|
|
11
|
+
}>;
|
|
12
|
+
/** Name of the backend being launched. Extensions should gate
|
|
13
|
+
* per-backend sections on this rather than settings.defaultBackend. */
|
|
14
|
+
activeBackend?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
2
18
|
export {};
|
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
|
-
|
|
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))
|
|
@@ -211,9 +60,18 @@ async function main() {
|
|
|
211
60
|
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
212
61
|
const core = createCore(config);
|
|
213
62
|
const { bus } = core;
|
|
214
|
-
// Track agent info from bus events (populated by extension backends)
|
|
215
63
|
let agentInfo = null;
|
|
216
|
-
bus.on("agent:info", (info) => {
|
|
64
|
+
bus.on("agent:info", (info) => {
|
|
65
|
+
agentInfo = info;
|
|
66
|
+
// Redraw so late agent:info emits (opencode-bridge after session.create) reach the prompt.
|
|
67
|
+
bus.emit("config:changed", {});
|
|
68
|
+
});
|
|
69
|
+
// tui-renderer subscribes to ui:error inside activateShell, after backend
|
|
70
|
+
// activation — pipe to stderr until the shell is up so boot failures surface.
|
|
71
|
+
const bootUiError = (e) => {
|
|
72
|
+
process.stderr.write(`agent-sh: ${e.message}\n`);
|
|
73
|
+
};
|
|
74
|
+
bus.on("ui:error", bootUiError);
|
|
217
75
|
// ── Interactive frontend ──────────────────────────────────────
|
|
218
76
|
if (process.env.DEBUG) {
|
|
219
77
|
console.error('[agent-sh] Setting up interactive frontend...');
|
|
@@ -293,6 +151,7 @@ async function main() {
|
|
|
293
151
|
"\n " + hint + "\n" +
|
|
294
152
|
borderLine + "\n\n");
|
|
295
153
|
}
|
|
154
|
+
await core.activateBackend(config.backend);
|
|
296
155
|
// 100ms sidesteps macOS SIGTTOU during fg-pgrp handoff.
|
|
297
156
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
298
157
|
shell = activateShell(extCtx, {
|
|
@@ -307,6 +166,7 @@ async function main() {
|
|
|
307
166
|
return { info: "" };
|
|
308
167
|
},
|
|
309
168
|
});
|
|
169
|
+
bus.off("ui:error", bootUiError);
|
|
310
170
|
bus.emit("input-mode:register", {
|
|
311
171
|
id: "agent",
|
|
312
172
|
trigger: ">",
|
|
@@ -318,7 +178,6 @@ async function main() {
|
|
|
318
178
|
},
|
|
319
179
|
returnToSelf: true,
|
|
320
180
|
});
|
|
321
|
-
core.activateBackend(config.backend);
|
|
322
181
|
// ── Terminal lifecycle ────────────────────────────────────────
|
|
323
182
|
process.on("SIGTERM", cleanup);
|
|
324
183
|
process.on("SIGHUP", cleanup);
|
|
@@ -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
|
+
}
|