agent-sh 0.4.0 → 0.5.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 +66 -113
- package/dist/agent/agent-loop.d.ts +85 -0
- package/dist/agent/agent-loop.js +611 -0
- package/dist/agent/conversation-state.d.ts +27 -0
- package/dist/agent/conversation-state.js +59 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +9 -0
- package/dist/agent/skills.d.ts +25 -0
- package/dist/agent/skills.js +186 -0
- package/dist/agent/subagent.d.ts +37 -0
- package/dist/agent/subagent.js +117 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +98 -0
- package/dist/agent/tool-registry.d.ts +15 -0
- package/dist/agent/tool-registry.js +30 -0
- package/dist/agent/tools/bash.d.ts +7 -0
- package/dist/agent/tools/bash.js +62 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +95 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +55 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +77 -0
- package/dist/agent/tools/list-skills.d.ts +2 -0
- package/dist/agent/tools/list-skills.js +28 -0
- package/dist/agent/tools/ls.d.ts +2 -0
- package/dist/agent/tools/ls.js +43 -0
- package/dist/agent/tools/read-file.d.ts +2 -0
- package/dist/agent/tools/read-file.js +55 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +57 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +74 -0
- package/dist/agent/types.d.ts +44 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +24 -14
- package/dist/core.js +260 -36
- package/dist/event-bus.d.ts +80 -14
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.js +12 -1
- package/dist/extensions/command-suggest.d.ts +10 -0
- package/dist/extensions/command-suggest.js +41 -0
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +161 -64
- package/dist/extensions/tui-renderer.js +90 -48
- package/dist/index.js +98 -122
- package/dist/input-handler.js +74 -7
- package/dist/output-parser.d.ts +7 -0
- package/dist/output-parser.js +27 -0
- package/dist/settings.d.ts +53 -2
- package/dist/settings.js +45 -2
- package/dist/shell.js +33 -26
- package/dist/types.d.ts +33 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.js +2 -2
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.js +15 -5
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +198 -0
- package/examples/extensions/claude-code-bridge/package.json +11 -0
- package/examples/extensions/openrouter.ts +87 -0
- package/examples/extensions/pi-bridge/README.md +35 -0
- package/examples/extensions/pi-bridge/index.ts +265 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/subagents.ts +87 -0
- package/package.json +3 -5
- package/dist/acp-client.d.ts +0 -105
- package/dist/acp-client.js +0 -684
- package/dist/extensions/shell-exec.d.ts +0 -24
- package/dist/extensions/shell-exec.js +0 -188
- package/dist/mcp-server.d.ts +0 -13
- package/dist/mcp-server.js +0 -234
- package/examples/pi-agent-sh.ts +0 -166
package/dist/index.js
CHANGED
|
@@ -8,16 +8,13 @@ import tuiRenderer from "./extensions/tui-renderer.js";
|
|
|
8
8
|
import slashCommands from "./extensions/slash-commands.js";
|
|
9
9
|
import fileAutocomplete from "./extensions/file-autocomplete.js";
|
|
10
10
|
import shellRecall from "./extensions/shell-recall.js";
|
|
11
|
-
import
|
|
11
|
+
import commandSuggest from "./extensions/command-suggest.js";
|
|
12
12
|
import { loadExtensions } from "./extension-loader.js";
|
|
13
|
+
import { getSettings } from "./settings.js";
|
|
13
14
|
/**
|
|
14
15
|
* Capture the user's full shell environment.
|
|
15
16
|
* This picks up env vars exported in .zshrc/.bashrc that the
|
|
16
17
|
* Node.js process doesn't have (e.g. when launched from an IDE).
|
|
17
|
-
*
|
|
18
|
-
* Uses -l (login shell) to get .zprofile/.bash_profile vars, then
|
|
19
|
-
* explicitly sources the interactive rc file (.zshrc/.bashrc) which
|
|
20
|
-
* -l alone doesn't load (that requires -i, which blocks on TTY).
|
|
21
18
|
*/
|
|
22
19
|
async function captureShellEnvAsync(shell) {
|
|
23
20
|
return new Promise((resolve) => {
|
|
@@ -37,7 +34,7 @@ async function captureShellEnvAsync(shell) {
|
|
|
37
34
|
});
|
|
38
35
|
child.on("close", (code) => {
|
|
39
36
|
if (code !== 0 || !output) {
|
|
40
|
-
resolve({});
|
|
37
|
+
resolve({});
|
|
41
38
|
return;
|
|
42
39
|
}
|
|
43
40
|
const env = {};
|
|
@@ -49,9 +46,8 @@ async function captureShellEnvAsync(shell) {
|
|
|
49
46
|
resolve(env);
|
|
50
47
|
});
|
|
51
48
|
child.on("error", () => {
|
|
52
|
-
resolve({});
|
|
49
|
+
resolve({});
|
|
53
50
|
});
|
|
54
|
-
// Safety timeout
|
|
55
51
|
setTimeout(() => {
|
|
56
52
|
child.kill("SIGTERM");
|
|
57
53
|
resolve({});
|
|
@@ -62,14 +58,9 @@ async function captureShellEnvAsync(shell) {
|
|
|
62
58
|
}
|
|
63
59
|
});
|
|
64
60
|
}
|
|
65
|
-
/**
|
|
66
|
-
* Merge captured shell env into base env, only adding keys that don't exist.
|
|
67
|
-
* This preserves any runtime modifications while adding missing shell vars.
|
|
68
|
-
*/
|
|
69
61
|
function mergeShellEnv(baseEnv, shellEnv) {
|
|
70
62
|
const merged = { ...baseEnv };
|
|
71
63
|
for (const [key, value] of Object.entries(shellEnv)) {
|
|
72
|
-
// Only add if key doesn't exist or is empty in base env
|
|
73
64
|
if (!(key in merged) || !merged[key]) {
|
|
74
65
|
merged[key] = value;
|
|
75
66
|
}
|
|
@@ -77,113 +68,96 @@ function mergeShellEnv(baseEnv, shellEnv) {
|
|
|
77
68
|
return merged;
|
|
78
69
|
}
|
|
79
70
|
function parseArgs(argv) {
|
|
80
|
-
// Priority: CLI args > Environment variables > Config file > Defaults
|
|
81
|
-
const defaultAgent = process.env.AGENT_SH_AGENT || "pi-acp";
|
|
82
|
-
let agentCommand = defaultAgent;
|
|
83
|
-
let agentArgs = [];
|
|
84
71
|
let model;
|
|
85
72
|
let extensions;
|
|
73
|
+
let provider;
|
|
86
74
|
const shell = process.env.SHELL || "/bin/bash";
|
|
75
|
+
let apiKey = process.env.OPENAI_API_KEY;
|
|
76
|
+
let baseURL = process.env.OPENAI_BASE_URL;
|
|
87
77
|
for (let i = 0; i < argv.length; i++) {
|
|
88
78
|
const arg = argv[i];
|
|
89
|
-
if (arg === "--
|
|
90
|
-
|
|
79
|
+
if (arg === "--model" && argv[i + 1]) {
|
|
80
|
+
model = argv[++i];
|
|
91
81
|
}
|
|
92
|
-
else if (arg === "--
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
else if (arg === "--api-key" && argv[i + 1]) {
|
|
83
|
+
apiKey = argv[++i];
|
|
84
|
+
}
|
|
85
|
+
else if (arg === "--base-url" && argv[i + 1]) {
|
|
86
|
+
baseURL = argv[++i];
|
|
87
|
+
}
|
|
88
|
+
else if (arg === "--provider" && argv[i + 1]) {
|
|
89
|
+
provider = argv[++i];
|
|
100
90
|
}
|
|
101
91
|
else if (arg === "--shell" && argv[i + 1]) {
|
|
102
|
-
return {
|
|
92
|
+
return { shell: argv[++i], model, extensions, apiKey, baseURL, provider };
|
|
103
93
|
}
|
|
104
94
|
else if ((arg === "--extensions" || arg === "-e") && argv[i + 1]) {
|
|
105
95
|
const exts = argv[++i].split(",").map(s => s.trim());
|
|
106
96
|
extensions = extensions ? [...extensions, ...exts] : exts;
|
|
107
97
|
}
|
|
108
98
|
else if (arg === "--help" || arg === "-h") {
|
|
109
|
-
console.log(`agent-sh — a shell-first terminal
|
|
99
|
+
console.log(`agent-sh — a shell-first terminal where AI is one keystroke away
|
|
110
100
|
|
|
111
101
|
Usage: agent-sh [options]
|
|
112
102
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
Provider Profiles:
|
|
104
|
+
--provider <name> Use a provider from ~/.agent-sh/settings.json
|
|
105
|
+
--model <name> Override default model
|
|
106
|
+
|
|
107
|
+
Direct LLM API:
|
|
108
|
+
--api-key <key> API key for OpenAI-compatible provider (or set OPENAI_API_KEY)
|
|
109
|
+
--base-url <url> Base URL for API (or set OPENAI_BASE_URL)
|
|
117
110
|
|
|
118
|
-
Options:
|
|
119
|
-
--agent <cmd> Agent command to launch (default: $AGENT_SH_AGENT or "pi-acp")
|
|
120
|
-
--agent-args <args> Arguments for the agent (space-separated, quoted)
|
|
111
|
+
General Options:
|
|
121
112
|
--shell <path> Shell to use (default: $SHELL or /bin/bash)
|
|
122
113
|
-e, --extensions Extensions to load (comma-separated, repeatable)
|
|
123
114
|
-h, --help Show this help
|
|
124
115
|
|
|
125
|
-
Extensions:
|
|
126
|
-
Extensions are loaded from (in order):
|
|
127
|
-
1. -e flags: npm packages or file paths
|
|
128
|
-
2. settings: ~/.agent-sh/settings.json → "extensions": [...]
|
|
129
|
-
3. directory: ~/.agent-sh/extensions/ (files or dirs with index.ts)
|
|
130
|
-
|
|
131
116
|
Environment Variables:
|
|
132
|
-
|
|
117
|
+
OPENAI_API_KEY API key for LLM provider
|
|
118
|
+
OPENAI_BASE_URL Base URL override (e.g., http://localhost:11434/v1 for Ollama)
|
|
133
119
|
|
|
134
120
|
Examples:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
121
|
+
# Use a configured provider
|
|
122
|
+
agent-sh --provider openai
|
|
123
|
+
|
|
124
|
+
# Direct API access
|
|
125
|
+
agent-sh --api-key "$KEY" --model gpt-4o
|
|
126
|
+
|
|
127
|
+
# Local model via Ollama
|
|
128
|
+
agent-sh --base-url http://localhost:11434/v1 --model llama3
|
|
138
129
|
|
|
139
130
|
Inside the shell:
|
|
140
131
|
Type normally Commands run in your real shell
|
|
141
|
-
> <query>
|
|
132
|
+
> <query> Ask the AI agent a question (execute mode)
|
|
133
|
+
? <command> Have the agent run a command in your shell (help mode)
|
|
142
134
|
> /help Show available slash commands
|
|
143
135
|
Ctrl-C Cancel agent response (or signal shell as usual)
|
|
144
136
|
`);
|
|
145
137
|
process.exit(0);
|
|
146
138
|
}
|
|
147
139
|
}
|
|
148
|
-
return {
|
|
149
|
-
}
|
|
150
|
-
function formatAgentInfo(agentInfo, model, thoughtLevel) {
|
|
151
|
-
const name = agentInfo.name.replace(/-acp$/, "").replace(/-/g, " ");
|
|
152
|
-
let infoStr = `${p.dim}${name}${p.reset}`;
|
|
153
|
-
if (model) {
|
|
154
|
-
const cleanModel = model
|
|
155
|
-
.replace(/^openai\//i, "")
|
|
156
|
-
.replace(/^anthropic\//i, "")
|
|
157
|
-
.replace(/^google\//i, "");
|
|
158
|
-
infoStr += ` ${p.dim}(${cleanModel})${p.reset}`;
|
|
159
|
-
}
|
|
160
|
-
if (thoughtLevel) {
|
|
161
|
-
// Clean up verbose mode names like "Thinking: medium" → "medium"
|
|
162
|
-
const label = thoughtLevel.replace(/^Thinking:\s*/i, "");
|
|
163
|
-
infoStr += ` ${p.dim}[${label}]${p.reset}`;
|
|
164
|
-
}
|
|
165
|
-
return infoStr;
|
|
140
|
+
return { shell, model, extensions, apiKey, baseURL, provider };
|
|
166
141
|
}
|
|
167
142
|
async function main() {
|
|
168
|
-
|
|
169
|
-
|
|
143
|
+
if (process.env.AGENT_SH) {
|
|
144
|
+
console.error("agent-sh: already running inside an agent-sh session (nested sessions are not supported).");
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
170
147
|
process.on("SIGTTOU", () => { });
|
|
171
|
-
// Also ignore SIGTTIN which can occur when reading from terminal while backgrounded.
|
|
172
148
|
process.on("SIGTTIN", () => { });
|
|
173
149
|
const config = parseArgs(process.argv.slice(2));
|
|
174
|
-
// Capture user's full shell environment
|
|
175
|
-
// This must complete before spawning the agent so it sees all env vars.
|
|
150
|
+
// Capture user's full shell environment
|
|
176
151
|
const baseEnv = {};
|
|
177
152
|
for (const [k, v] of Object.entries(process.env)) {
|
|
178
153
|
if (v !== undefined)
|
|
179
154
|
baseEnv[k] = v;
|
|
180
155
|
}
|
|
181
|
-
config.shellEnv = baseEnv;
|
|
182
156
|
const shellPath = config.shell || process.env.SHELL || "/bin/bash";
|
|
183
157
|
try {
|
|
184
158
|
const shellEnv = await captureShellEnvAsync(shellPath);
|
|
185
159
|
if (Object.keys(shellEnv).length > 0) {
|
|
186
|
-
|
|
160
|
+
Object.assign(baseEnv, mergeShellEnv(baseEnv, shellEnv));
|
|
187
161
|
if (process.env.DEBUG) {
|
|
188
162
|
console.error('[agent-sh] Shell environment captured');
|
|
189
163
|
}
|
|
@@ -194,7 +168,10 @@ async function main() {
|
|
|
194
168
|
}
|
|
195
169
|
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
196
170
|
const core = createCore(config);
|
|
197
|
-
const { bus
|
|
171
|
+
const { bus } = core;
|
|
172
|
+
// Track agent info from bus events (populated by extension backends)
|
|
173
|
+
let agentInfo = null;
|
|
174
|
+
bus.on("agent:info", (info) => { agentInfo = info; });
|
|
198
175
|
// ── Interactive frontend ──────────────────────────────────────
|
|
199
176
|
if (process.env.DEBUG) {
|
|
200
177
|
console.error('[agent-sh] Setting up interactive frontend...');
|
|
@@ -213,8 +190,6 @@ async function main() {
|
|
|
213
190
|
if (process.env.DEBUG) {
|
|
214
191
|
console.error('[agent-sh] Creating Shell...');
|
|
215
192
|
}
|
|
216
|
-
// Small delay on macOS to ensure we're fully in the foreground process group
|
|
217
|
-
// before spawning the PTY. This prevents SIGTTOU suspension.
|
|
218
193
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
219
194
|
const shell = new Shell({
|
|
220
195
|
bus,
|
|
@@ -223,13 +198,11 @@ async function main() {
|
|
|
223
198
|
shell: config.shell || process.env.SHELL || "/bin/bash",
|
|
224
199
|
cwd: process.cwd(),
|
|
225
200
|
onShowAgentInfo: () => {
|
|
226
|
-
if (
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return { info: formatAgentInfo(agentInfo, model, mode?.name ?? null) };
|
|
232
|
-
}
|
|
201
|
+
if (agentInfo) {
|
|
202
|
+
return { info: `${p.dim}${agentInfo.name}${agentInfo.model ? ` (${agentInfo.model})` : ""}${p.reset}` };
|
|
203
|
+
}
|
|
204
|
+
if (core.llmClient) {
|
|
205
|
+
return { info: `${p.dim}agent-sh (${core.llmClient.model})${p.reset}` };
|
|
233
206
|
}
|
|
234
207
|
return { info: "" };
|
|
235
208
|
},
|
|
@@ -239,24 +212,35 @@ async function main() {
|
|
|
239
212
|
}
|
|
240
213
|
// ── Input modes ──────────────────────────────────────────────
|
|
241
214
|
bus.emit("input-mode:register", {
|
|
242
|
-
id: "
|
|
243
|
-
trigger: "
|
|
244
|
-
label: "
|
|
215
|
+
id: "execute",
|
|
216
|
+
trigger: ">",
|
|
217
|
+
label: "execute",
|
|
245
218
|
promptIcon: "❯",
|
|
246
|
-
indicator: "
|
|
219
|
+
indicator: "●",
|
|
247
220
|
onSubmit(query, b) {
|
|
248
|
-
b.emit("agent:submit", { query, modeLabel: "
|
|
221
|
+
b.emit("agent:submit", { query, modeLabel: "Execute", modeInstruction: "[mode: execute]" });
|
|
249
222
|
},
|
|
250
223
|
returnToSelf: true,
|
|
251
224
|
});
|
|
252
225
|
bus.emit("input-mode:register", {
|
|
253
|
-
id: "
|
|
254
|
-
trigger: "
|
|
255
|
-
label: "
|
|
256
|
-
promptIcon: "
|
|
257
|
-
indicator: "
|
|
226
|
+
id: "help",
|
|
227
|
+
trigger: "?",
|
|
228
|
+
label: "help",
|
|
229
|
+
promptIcon: "❯",
|
|
230
|
+
indicator: "❓",
|
|
258
231
|
onSubmit(query, b) {
|
|
259
|
-
|
|
232
|
+
const onToolDone = (e) => {
|
|
233
|
+
if (e.kind === "execute") {
|
|
234
|
+
b.emit("agent:cancel-request", { silent: true });
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const cleanup = () => {
|
|
238
|
+
b.off("agent:tool-completed", onToolDone);
|
|
239
|
+
b.off("agent:processing-done", cleanup);
|
|
240
|
+
};
|
|
241
|
+
b.on("agent:tool-completed", onToolDone);
|
|
242
|
+
b.on("agent:processing-done", cleanup);
|
|
243
|
+
b.emit("agent:submit", { query, modeLabel: "Help", modeInstruction: "[mode: help]" });
|
|
260
244
|
},
|
|
261
245
|
returnToSelf: false,
|
|
262
246
|
});
|
|
@@ -269,20 +253,12 @@ async function main() {
|
|
|
269
253
|
slashCommands(extCtx);
|
|
270
254
|
fileAutocomplete(extCtx);
|
|
271
255
|
shellRecall(extCtx);
|
|
272
|
-
|
|
273
|
-
//
|
|
274
|
-
const tmpDir = shell.getTmpDir();
|
|
275
|
-
if (tmpDir) {
|
|
276
|
-
if (process.env.DEBUG) {
|
|
277
|
-
console.error('[agent-sh] Starting shell-exec socket server...');
|
|
278
|
-
}
|
|
279
|
-
shellExec(extCtx, { socketPath: `${tmpDir}/shell.sock` });
|
|
280
|
-
}
|
|
281
|
-
// Load extensions with timeout to prevent blocking startup
|
|
256
|
+
commandSuggest(extCtx);
|
|
257
|
+
// Load user extensions (may register alternative agent backends)
|
|
282
258
|
if (process.env.DEBUG) {
|
|
283
259
|
console.error('[agent-sh] Loading extensions...');
|
|
284
260
|
}
|
|
285
|
-
const loadExtensionsTimeoutMs = 10000;
|
|
261
|
+
const loadExtensionsTimeoutMs = 10000;
|
|
286
262
|
await Promise.race([
|
|
287
263
|
loadExtensions(extCtx, config.extensions),
|
|
288
264
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
|
|
@@ -292,21 +268,28 @@ async function main() {
|
|
|
292
268
|
if (process.env.DEBUG) {
|
|
293
269
|
console.error('[agent-sh] Extensions loaded');
|
|
294
270
|
}
|
|
295
|
-
// ──
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
271
|
+
// ── Activate agent backend ────────────────────────────────────
|
|
272
|
+
// Extensions had their chance to register via agent:register-backend.
|
|
273
|
+
// If none did, the built-in AgentLoop gets wired to bus events.
|
|
274
|
+
core.activateBackend();
|
|
275
|
+
// ── Startup banner ───────────────────────────────────────────
|
|
276
|
+
const settings = getSettings();
|
|
277
|
+
if (settings.startupBanner !== false) {
|
|
278
|
+
const title = core.llmClient
|
|
279
|
+
? `${p.accent}${p.bold}agent-sh${p.reset}${p.dim} · ${core.llmClient.model}${p.reset}`
|
|
280
|
+
: `${p.accent}${p.bold}agent-sh${p.reset}`;
|
|
281
|
+
const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}?${p.muted} to run in shell · ${p.warning}/help${p.muted} for commands${p.reset}`;
|
|
282
|
+
const termW = process.stdout.columns || 80;
|
|
283
|
+
const borderLine = `${p.muted}${"─".repeat(Math.min(termW, 60))}${p.reset}`;
|
|
284
|
+
process.stdout.write("\n" + borderLine + "\n" +
|
|
285
|
+
" " + title + "\n" +
|
|
286
|
+
" " + hint + "\n" +
|
|
287
|
+
borderLine + "\n\n");
|
|
288
|
+
}
|
|
303
289
|
// ── Terminal lifecycle ────────────────────────────────────────
|
|
304
290
|
process.on("SIGTERM", cleanup);
|
|
305
291
|
process.on("SIGHUP", cleanup);
|
|
306
|
-
// Handle terminal stop/resume signals properly
|
|
307
292
|
process.on("SIGTSTP", () => {
|
|
308
|
-
// Handle Ctrl+Z - suspend the entire process group
|
|
309
|
-
// Restore terminal state before suspending
|
|
310
293
|
if (process.stdin.isTTY) {
|
|
311
294
|
try {
|
|
312
295
|
process.stdin.setRawMode(false);
|
|
@@ -315,14 +298,11 @@ async function main() {
|
|
|
315
298
|
// Ignore
|
|
316
299
|
}
|
|
317
300
|
}
|
|
318
|
-
// Re-send SIGSTOP to actually suspend
|
|
319
301
|
process.kill(process.pid, "SIGSTOP");
|
|
320
302
|
});
|
|
321
303
|
process.on("SIGCONT", () => {
|
|
322
|
-
// Re-acquire terminal when brought back to foreground
|
|
323
304
|
if (process.stdin.isTTY) {
|
|
324
305
|
try {
|
|
325
|
-
// Ensure we reacquire controlling terminal
|
|
326
306
|
process.stdin.setRawMode(true);
|
|
327
307
|
}
|
|
328
308
|
catch {
|
|
@@ -340,17 +320,14 @@ async function main() {
|
|
|
340
320
|
}
|
|
341
321
|
process.exit(e.exitCode);
|
|
342
322
|
});
|
|
343
|
-
// Set up stdin - resume after all event listeners are in place
|
|
344
323
|
if (process.env.DEBUG) {
|
|
345
324
|
console.error('[agent-sh] Resuming stdin...');
|
|
346
325
|
}
|
|
347
326
|
process.stdin.resume();
|
|
348
|
-
// Set raw mode after resume to avoid SIGTTOU issues
|
|
349
327
|
if (process.stdin.isTTY) {
|
|
350
328
|
if (process.env.DEBUG) {
|
|
351
329
|
console.error('[agent-sh] Setting raw mode...');
|
|
352
330
|
}
|
|
353
|
-
// Use setImmediate to ensure we're in the next tick
|
|
354
331
|
setImmediate(() => {
|
|
355
332
|
try {
|
|
356
333
|
process.stdin.setRawMode(true);
|
|
@@ -362,7 +339,6 @@ async function main() {
|
|
|
362
339
|
if (process.env.DEBUG) {
|
|
363
340
|
console.error(`[agent-sh] Failed to set raw mode: ${err}`);
|
|
364
341
|
}
|
|
365
|
-
// May fail if process is in background; SIGTTOU handler prevents suspension
|
|
366
342
|
}
|
|
367
343
|
});
|
|
368
344
|
}
|
package/dist/input-handler.js
CHANGED
|
@@ -187,8 +187,44 @@ export class InputHandler {
|
|
|
187
187
|
this.lineBuffer = "";
|
|
188
188
|
this.ctx.writeToPty(ch);
|
|
189
189
|
}
|
|
190
|
+
else if (ch === "\x1b") {
|
|
191
|
+
// Escape sequence — forward the entire sequence to the PTY but
|
|
192
|
+
// don't let it corrupt lineBuffer. Skip CSI (ESC [ ... final)
|
|
193
|
+
// and SS3 (ESC O <char>) sequences; anything else: just ESC.
|
|
194
|
+
let seq = ch;
|
|
195
|
+
if (i + 1 < data.length) {
|
|
196
|
+
const next = data[i + 1];
|
|
197
|
+
if (next === "[") {
|
|
198
|
+
// CSI: ESC [ (params) (intermediates) final_byte
|
|
199
|
+
seq += next;
|
|
200
|
+
i++;
|
|
201
|
+
while (i + 1 < data.length && data[i + 1].charCodeAt(0) < 0x40) {
|
|
202
|
+
i++;
|
|
203
|
+
seq += data[i];
|
|
204
|
+
}
|
|
205
|
+
if (i + 1 < data.length) {
|
|
206
|
+
i++;
|
|
207
|
+
seq += data[i];
|
|
208
|
+
} // final byte
|
|
209
|
+
}
|
|
210
|
+
else if (next === "O") {
|
|
211
|
+
// SS3: ESC O <char>
|
|
212
|
+
seq += next;
|
|
213
|
+
i++;
|
|
214
|
+
if (i + 1 < data.length) {
|
|
215
|
+
i++;
|
|
216
|
+
seq += data[i];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// ESC + single char (alt-key, etc.)
|
|
221
|
+
seq += next;
|
|
222
|
+
i++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
this.ctx.writeToPty(seq);
|
|
226
|
+
}
|
|
190
227
|
else if (ch.charCodeAt(0) < 32 && ch !== "\t") {
|
|
191
|
-
this.lineBuffer = "";
|
|
192
228
|
this.ctx.writeToPty(ch);
|
|
193
229
|
}
|
|
194
230
|
else {
|
|
@@ -253,8 +289,20 @@ export class InputHandler {
|
|
|
253
289
|
this.updateAutocomplete();
|
|
254
290
|
}
|
|
255
291
|
updateAutocomplete() {
|
|
292
|
+
const buf = this.editor.buffer;
|
|
293
|
+
let command = null;
|
|
294
|
+
let commandArgs = null;
|
|
295
|
+
if (buf.startsWith("/")) {
|
|
296
|
+
const spaceIdx = buf.indexOf(" ");
|
|
297
|
+
if (spaceIdx !== -1) {
|
|
298
|
+
command = buf.slice(0, spaceIdx);
|
|
299
|
+
commandArgs = buf.slice(spaceIdx + 1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
256
302
|
const { items } = this.bus.emitPipe("autocomplete:request", {
|
|
257
|
-
buffer:
|
|
303
|
+
buffer: buf,
|
|
304
|
+
command,
|
|
305
|
+
commandArgs,
|
|
258
306
|
items: [],
|
|
259
307
|
});
|
|
260
308
|
if (items.length > 0) {
|
|
@@ -289,10 +337,15 @@ export class InputHandler {
|
|
|
289
337
|
if (this.autocompleteLines > 0) {
|
|
290
338
|
process.stdout.write(`\x1b[${this.autocompleteLines}A`);
|
|
291
339
|
}
|
|
340
|
+
// Reposition cursor: must match the layout in writeModePromptLine()
|
|
292
341
|
const agentInfo = this.onShowAgentInfo();
|
|
293
|
-
const
|
|
342
|
+
const indicator = this.activeMode?.indicator ?? "●";
|
|
343
|
+
const infoPrefix = agentInfo.info
|
|
344
|
+
? `${agentInfo.info} ${indicator} `
|
|
345
|
+
: `${indicator} `;
|
|
294
346
|
const icon = this.activeMode?.promptIcon ?? "❯";
|
|
295
|
-
const
|
|
347
|
+
const promptVisLen = visibleLen(infoPrefix) + visibleLen(icon) + 1;
|
|
348
|
+
const col = promptVisLen + this.editor.cursor;
|
|
296
349
|
process.stdout.write(`\r\x1b[${col}C`);
|
|
297
350
|
}
|
|
298
351
|
applyAutocomplete() {
|
|
@@ -359,16 +412,30 @@ export class InputHandler {
|
|
|
359
412
|
processModeActions(actions) {
|
|
360
413
|
for (const act of actions) {
|
|
361
414
|
switch (act.action) {
|
|
362
|
-
case "changed":
|
|
415
|
+
case "changed": {
|
|
416
|
+
// If the buffer is exactly a trigger char for a different mode, switch to it
|
|
417
|
+
const switchMode = this.modes.get(this.editor.buffer);
|
|
418
|
+
if (this.editor.buffer.length === 1 && switchMode && switchMode !== this.activeMode) {
|
|
419
|
+
this.dismissAutocomplete();
|
|
420
|
+
this.clearPromptArea();
|
|
421
|
+
this.activeMode = switchMode;
|
|
422
|
+
this.editor.clear();
|
|
423
|
+
this.writeModePromptLine(false);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
363
426
|
this.historyIndex = -1;
|
|
364
427
|
this.autocompleteIndex = 0;
|
|
365
428
|
this.renderModeInput();
|
|
366
429
|
break;
|
|
430
|
+
}
|
|
367
431
|
case "submit": {
|
|
368
432
|
if (this.autocompleteActive) {
|
|
369
433
|
this.applyAutocomplete();
|
|
370
434
|
}
|
|
371
|
-
|
|
435
|
+
// Use editor.buffer (not act.buffer) so autocomplete selections
|
|
436
|
+
// take effect — act.buffer is a stale snapshot from before
|
|
437
|
+
// applyAutocomplete() updated the buffer.
|
|
438
|
+
const query = this.editor.buffer.trim();
|
|
372
439
|
if (query) {
|
|
373
440
|
// Add to history (avoid consecutive duplicates)
|
|
374
441
|
if (this.history.length === 0 || this.history[this.history.length - 1] !== query) {
|
|
@@ -390,7 +457,7 @@ export class InputHandler {
|
|
|
390
457
|
const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
|
|
391
458
|
const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
|
|
392
459
|
this.bus.emit("command:execute", { name, args });
|
|
393
|
-
this.ctx.
|
|
460
|
+
this.ctx.freshPrompt();
|
|
394
461
|
}
|
|
395
462
|
else if (query) {
|
|
396
463
|
this.pendingReturnMode = currentMode.returnToSelf ? currentMode.id : null;
|
package/dist/output-parser.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ export declare class OutputParser {
|
|
|
19
19
|
isPromptReady(): boolean;
|
|
20
20
|
isForegroundBusy(): boolean;
|
|
21
21
|
getCwd(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Detect preexec marker (OSC 9997) emitted by the shell's preexec hook.
|
|
24
|
+
* This carries the actual command text from the shell — more reliable than
|
|
25
|
+
* the InputHandler's lineBuffer which can't track history recall or tab
|
|
26
|
+
* completion. Returns data with the OSC stripped out.
|
|
27
|
+
*/
|
|
28
|
+
private handlePreexec;
|
|
22
29
|
private parseOSC7;
|
|
23
30
|
/**
|
|
24
31
|
* Detect our custom prompt marker (OSC 9999) in the PTY stream.
|
package/dist/output-parser.js
CHANGED
|
@@ -17,6 +17,7 @@ export class OutputParser {
|
|
|
17
17
|
/** Process a chunk of PTY output data. */
|
|
18
18
|
processData(data) {
|
|
19
19
|
this.parseOSC7(data);
|
|
20
|
+
data = this.handlePreexec(data);
|
|
20
21
|
this.parsePromptMarker(data);
|
|
21
22
|
this.parsePromptEnd(data);
|
|
22
23
|
}
|
|
@@ -41,6 +42,32 @@ export class OutputParser {
|
|
|
41
42
|
return this.cwd;
|
|
42
43
|
}
|
|
43
44
|
// ── Parsing ─────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Detect preexec marker (OSC 9997) emitted by the shell's preexec hook.
|
|
47
|
+
* This carries the actual command text from the shell — more reliable than
|
|
48
|
+
* the InputHandler's lineBuffer which can't track history recall or tab
|
|
49
|
+
* completion. Returns data with the OSC stripped out.
|
|
50
|
+
*/
|
|
51
|
+
handlePreexec(data) {
|
|
52
|
+
const marker = "\x1b]9997;";
|
|
53
|
+
const idx = data.indexOf(marker);
|
|
54
|
+
if (idx === -1)
|
|
55
|
+
return data;
|
|
56
|
+
const endIdx = data.indexOf("\x07", idx + marker.length);
|
|
57
|
+
if (endIdx === -1)
|
|
58
|
+
return data; // incomplete OSC, wait for next chunk
|
|
59
|
+
const command = data.slice(idx + marker.length, endIdx);
|
|
60
|
+
// Authoritative command from the shell — override any lineBuffer guess
|
|
61
|
+
this.lastCommand = command;
|
|
62
|
+
this.currentOutputCapture = ""; // discard echoed text accumulated before preexec
|
|
63
|
+
if (!this.foregroundBusy) {
|
|
64
|
+
this.foregroundBusy = true;
|
|
65
|
+
this.bus.emit("shell:foreground-busy", { busy: true });
|
|
66
|
+
}
|
|
67
|
+
this.bus.emit("shell:command-start", { command, cwd: this.cwd });
|
|
68
|
+
// Return only data after the OSC — everything before was the echo
|
|
69
|
+
return data.slice(endIdx + 1);
|
|
70
|
+
}
|
|
44
71
|
parseOSC7(data) {
|
|
45
72
|
const match = data.match(/\x1b\]7;file:\/\/[^/]*(\/[^\x07\x1b]*)/);
|
|
46
73
|
if (match?.[1]) {
|
package/dist/settings.d.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
export declare const CONFIG_DIR: string;
|
|
2
|
+
/** Provider profile — a named LLM configuration. */
|
|
3
|
+
export interface ProviderConfig {
|
|
4
|
+
/** API key (supports $ENV_VAR syntax for runtime expansion). */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** Base URL for OpenAI-compatible API. */
|
|
7
|
+
baseURL?: string;
|
|
8
|
+
/** Default model to use. Falls back to first entry in models list. */
|
|
9
|
+
defaultModel?: string;
|
|
10
|
+
/** Models available for cycling. */
|
|
11
|
+
models?: string[];
|
|
12
|
+
/** Context window size in tokens (e.g. 128000). Used for usage display. */
|
|
13
|
+
contextWindow?: number;
|
|
14
|
+
}
|
|
2
15
|
export interface Settings {
|
|
3
16
|
/** Extensions to load (npm packages or file paths). */
|
|
4
17
|
extensions?: string[];
|
|
5
18
|
/** Max agent query history entries to keep. */
|
|
6
19
|
historySize?: number;
|
|
20
|
+
/** Named provider configurations. */
|
|
21
|
+
providers?: Record<string, ProviderConfig>;
|
|
22
|
+
/** Which provider to use by default. */
|
|
23
|
+
defaultProvider?: string;
|
|
24
|
+
/** Preferred agent backend (extension name, e.g. "pi", "claude-code"). */
|
|
25
|
+
defaultBackend?: string;
|
|
7
26
|
/** Recent exchanges included in agent context window. */
|
|
8
27
|
contextWindowSize?: number;
|
|
9
28
|
/** Context budget in bytes (~4 chars per token). */
|
|
@@ -22,8 +41,12 @@ export interface Settings {
|
|
|
22
41
|
readOutputMaxLines?: number;
|
|
23
42
|
/** Max diff lines shown before "ctrl+o to expand". */
|
|
24
43
|
diffMaxLines?: number;
|
|
25
|
-
/**
|
|
26
|
-
|
|
44
|
+
/** Additional directories to scan for skills (supports ~ expansion). */
|
|
45
|
+
skillPaths?: string[];
|
|
46
|
+
/** Show a startup banner when agent-sh launches. */
|
|
47
|
+
startupBanner?: boolean;
|
|
48
|
+
/** Show a subtle agent-sh indicator in the shell prompt. */
|
|
49
|
+
promptIndicator?: boolean;
|
|
27
50
|
}
|
|
28
51
|
declare const DEFAULTS: Required<Settings>;
|
|
29
52
|
/** Load settings from disk (cached after first call). */
|
|
@@ -41,4 +64,32 @@ export declare function getSettings(): Settings & typeof DEFAULTS;
|
|
|
41
64
|
export declare function getExtensionSettings<T extends Record<string, unknown>>(namespace: string, defaults: T): T;
|
|
42
65
|
/** Reset cached settings (for testing or after external edit). */
|
|
43
66
|
export declare function reloadSettings(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Expand $ENV_VAR references in a string.
|
|
69
|
+
* Supports $VAR and ${VAR} syntax.
|
|
70
|
+
*/
|
|
71
|
+
export declare function expandEnvVars(value: string): string;
|
|
72
|
+
/** Resolved provider ready for use (env vars expanded, defaults applied). */
|
|
73
|
+
export interface ResolvedProvider {
|
|
74
|
+
id: string;
|
|
75
|
+
apiKey?: string;
|
|
76
|
+
baseURL?: string;
|
|
77
|
+
defaultModel?: string;
|
|
78
|
+
models: string[];
|
|
79
|
+
contextWindow?: number;
|
|
80
|
+
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
81
|
+
supportsReasoningEffort?: boolean;
|
|
82
|
+
/** Per-model capabilities, keyed by model id. */
|
|
83
|
+
modelCapabilities?: Map<string, {
|
|
84
|
+
reasoning?: boolean;
|
|
85
|
+
contextWindow?: number;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a provider config by name from settings.
|
|
90
|
+
* Returns null if provider not found.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveProvider(name: string): ResolvedProvider | null;
|
|
93
|
+
/** Get all configured provider names. */
|
|
94
|
+
export declare function getProviderNames(): string[];
|
|
44
95
|
export {};
|