agent-sh 0.4.0 → 0.6.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 +37 -115
- package/dist/agent/agent-loop.d.ts +86 -0
- package/dist/agent/agent-loop.js +704 -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 +119 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +103 -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 +71 -0
- package/dist/agent/tools/display.d.ts +13 -0
- package/dist/agent/tools/display.js +70 -0
- package/dist/agent/tools/edit-file.d.ts +2 -0
- package/dist/agent/tools/edit-file.js +148 -0
- package/dist/agent/tools/glob.d.ts +2 -0
- package/dist/agent/tools/glob.js +87 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +168 -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 +72 -0
- package/dist/agent/tools/read-file.d.ts +10 -0
- package/dist/agent/tools/read-file.js +101 -0
- package/dist/agent/tools/user-shell.d.ts +13 -0
- package/dist/agent/tools/user-shell.js +84 -0
- package/dist/agent/tools/write-file.d.ts +2 -0
- package/dist/agent/tools/write-file.js +82 -0
- package/dist/agent/types.d.ts +78 -0
- package/dist/agent/types.js +1 -0
- package/dist/core.d.ts +22 -14
- package/dist/core.js +256 -36
- package/dist/event-bus.d.ts +98 -17
- package/dist/event-bus.js +10 -1
- package/dist/extension-loader.d.ts +1 -1
- package/dist/extension-loader.js +10 -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 +426 -126
- package/dist/index.js +110 -129
- package/dist/input-handler.js +78 -9
- 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 +46 -3
- package/dist/shell.js +35 -28
- 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/diff.js +10 -0
- package/dist/utils/llm-client.d.ts +45 -0
- package/dist/utils/llm-client.js +60 -0
- package/dist/utils/markdown.d.ts +1 -0
- package/dist/utils/markdown.js +25 -3
- package/dist/utils/stream-transform.js +20 -47
- package/dist/utils/tool-display.d.ts +4 -0
- package/dist/utils/tool-display.js +35 -8
- package/examples/extensions/claude-code-bridge/README.md +35 -0
- package/examples/extensions/claude-code-bridge/index.ts +194 -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 +263 -0
- package/examples/extensions/pi-bridge/package.json +13 -0
- package/examples/extensions/secret-guard.ts +100 -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,14 @@ 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";
|
|
14
|
+
import { discoverSkills } from "./agent/skills.js";
|
|
13
15
|
/**
|
|
14
16
|
* Capture the user's full shell environment.
|
|
15
17
|
* This picks up env vars exported in .zshrc/.bashrc that the
|
|
16
18
|
* 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
19
|
*/
|
|
22
20
|
async function captureShellEnvAsync(shell) {
|
|
23
21
|
return new Promise((resolve) => {
|
|
@@ -37,7 +35,7 @@ async function captureShellEnvAsync(shell) {
|
|
|
37
35
|
});
|
|
38
36
|
child.on("close", (code) => {
|
|
39
37
|
if (code !== 0 || !output) {
|
|
40
|
-
resolve({});
|
|
38
|
+
resolve({});
|
|
41
39
|
return;
|
|
42
40
|
}
|
|
43
41
|
const env = {};
|
|
@@ -49,9 +47,8 @@ async function captureShellEnvAsync(shell) {
|
|
|
49
47
|
resolve(env);
|
|
50
48
|
});
|
|
51
49
|
child.on("error", () => {
|
|
52
|
-
resolve({});
|
|
50
|
+
resolve({});
|
|
53
51
|
});
|
|
54
|
-
// Safety timeout
|
|
55
52
|
setTimeout(() => {
|
|
56
53
|
child.kill("SIGTERM");
|
|
57
54
|
resolve({});
|
|
@@ -62,14 +59,9 @@ async function captureShellEnvAsync(shell) {
|
|
|
62
59
|
}
|
|
63
60
|
});
|
|
64
61
|
}
|
|
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
62
|
function mergeShellEnv(baseEnv, shellEnv) {
|
|
70
63
|
const merged = { ...baseEnv };
|
|
71
64
|
for (const [key, value] of Object.entries(shellEnv)) {
|
|
72
|
-
// Only add if key doesn't exist or is empty in base env
|
|
73
65
|
if (!(key in merged) || !merged[key]) {
|
|
74
66
|
merged[key] = value;
|
|
75
67
|
}
|
|
@@ -77,113 +69,95 @@ function mergeShellEnv(baseEnv, shellEnv) {
|
|
|
77
69
|
return merged;
|
|
78
70
|
}
|
|
79
71
|
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
72
|
let model;
|
|
85
73
|
let extensions;
|
|
74
|
+
let provider;
|
|
86
75
|
const shell = process.env.SHELL || "/bin/bash";
|
|
76
|
+
let apiKey = process.env.OPENAI_API_KEY;
|
|
77
|
+
let baseURL = process.env.OPENAI_BASE_URL;
|
|
87
78
|
for (let i = 0; i < argv.length; i++) {
|
|
88
79
|
const arg = argv[i];
|
|
89
|
-
if (arg === "--
|
|
90
|
-
|
|
80
|
+
if (arg === "--model" && argv[i + 1]) {
|
|
81
|
+
model = argv[++i];
|
|
91
82
|
}
|
|
92
|
-
else if (arg === "--
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
83
|
+
else if (arg === "--api-key" && argv[i + 1]) {
|
|
84
|
+
apiKey = argv[++i];
|
|
85
|
+
}
|
|
86
|
+
else if (arg === "--base-url" && argv[i + 1]) {
|
|
87
|
+
baseURL = argv[++i];
|
|
88
|
+
}
|
|
89
|
+
else if (arg === "--provider" && argv[i + 1]) {
|
|
90
|
+
provider = argv[++i];
|
|
100
91
|
}
|
|
101
92
|
else if (arg === "--shell" && argv[i + 1]) {
|
|
102
|
-
return {
|
|
93
|
+
return { shell: argv[++i], model, extensions, apiKey, baseURL, provider };
|
|
103
94
|
}
|
|
104
95
|
else if ((arg === "--extensions" || arg === "-e") && argv[i + 1]) {
|
|
105
96
|
const exts = argv[++i].split(",").map(s => s.trim());
|
|
106
97
|
extensions = extensions ? [...extensions, ...exts] : exts;
|
|
107
98
|
}
|
|
108
99
|
else if (arg === "--help" || arg === "-h") {
|
|
109
|
-
console.log(`agent-sh — a shell-first terminal
|
|
100
|
+
console.log(`agent-sh — a shell-first terminal where AI is one keystroke away
|
|
110
101
|
|
|
111
102
|
Usage: agent-sh [options]
|
|
112
103
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
Provider Profiles:
|
|
105
|
+
--provider <name> Use a provider from ~/.agent-sh/settings.json
|
|
106
|
+
--model <name> Override default model
|
|
107
|
+
|
|
108
|
+
Direct LLM API:
|
|
109
|
+
--api-key <key> API key for OpenAI-compatible provider (or set OPENAI_API_KEY)
|
|
110
|
+
--base-url <url> Base URL for API (or set OPENAI_BASE_URL)
|
|
117
111
|
|
|
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)
|
|
112
|
+
General Options:
|
|
121
113
|
--shell <path> Shell to use (default: $SHELL or /bin/bash)
|
|
122
114
|
-e, --extensions Extensions to load (comma-separated, repeatable)
|
|
123
115
|
-h, --help Show this help
|
|
124
116
|
|
|
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
117
|
Environment Variables:
|
|
132
|
-
|
|
118
|
+
OPENAI_API_KEY API key for LLM provider
|
|
119
|
+
OPENAI_BASE_URL Base URL override (e.g., http://localhost:11434/v1 for Ollama)
|
|
133
120
|
|
|
134
121
|
Examples:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
122
|
+
# Use a configured provider
|
|
123
|
+
agent-sh --provider openai
|
|
124
|
+
|
|
125
|
+
# Direct API access
|
|
126
|
+
agent-sh --api-key "$KEY" --model gpt-4o
|
|
127
|
+
|
|
128
|
+
# Local model via Ollama
|
|
129
|
+
agent-sh --base-url http://localhost:11434/v1 --model llama3
|
|
138
130
|
|
|
139
131
|
Inside the shell:
|
|
140
132
|
Type normally Commands run in your real shell
|
|
141
|
-
> <query>
|
|
133
|
+
> <query> Ask the AI agent (it decides how to help)
|
|
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
|
},
|
|
@@ -237,28 +210,17 @@ async function main() {
|
|
|
237
210
|
if (process.env.DEBUG) {
|
|
238
211
|
console.error('[agent-sh] Shell created');
|
|
239
212
|
}
|
|
240
|
-
// ── Input
|
|
213
|
+
// ── Input mode ───────────────────────────────────────────────
|
|
241
214
|
bus.emit("input-mode:register", {
|
|
242
|
-
id: "
|
|
243
|
-
trigger: "?",
|
|
244
|
-
label: "query",
|
|
245
|
-
promptIcon: "❯",
|
|
246
|
-
indicator: "❓",
|
|
247
|
-
onSubmit(query, b) {
|
|
248
|
-
b.emit("agent:submit", { query, modeLabel: "Query", modeInstruction: "[mode: query]" });
|
|
249
|
-
},
|
|
250
|
-
returnToSelf: true,
|
|
251
|
-
});
|
|
252
|
-
bus.emit("input-mode:register", {
|
|
253
|
-
id: "execute",
|
|
215
|
+
id: "agent",
|
|
254
216
|
trigger: ">",
|
|
255
|
-
label: "
|
|
256
|
-
promptIcon: "
|
|
217
|
+
label: "agent",
|
|
218
|
+
promptIcon: "❯",
|
|
257
219
|
indicator: "●",
|
|
258
220
|
onSubmit(query, b) {
|
|
259
|
-
b.emit("agent:submit", { query
|
|
221
|
+
b.emit("agent:submit", { query });
|
|
260
222
|
},
|
|
261
|
-
returnToSelf:
|
|
223
|
+
returnToSelf: true,
|
|
262
224
|
});
|
|
263
225
|
// ── Extensions ────────────────────────────────────────────────
|
|
264
226
|
if (process.env.DEBUG) {
|
|
@@ -269,22 +231,15 @@ async function main() {
|
|
|
269
231
|
slashCommands(extCtx);
|
|
270
232
|
fileAutocomplete(extCtx);
|
|
271
233
|
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
|
|
234
|
+
commandSuggest(extCtx);
|
|
235
|
+
// Load user extensions (may register alternative agent backends)
|
|
282
236
|
if (process.env.DEBUG) {
|
|
283
237
|
console.error('[agent-sh] Loading extensions...');
|
|
284
238
|
}
|
|
285
|
-
const loadExtensionsTimeoutMs = 10000;
|
|
239
|
+
const loadExtensionsTimeoutMs = 10000;
|
|
240
|
+
let loadedExtensions = [];
|
|
286
241
|
await Promise.race([
|
|
287
|
-
loadExtensions(extCtx, config.extensions),
|
|
242
|
+
loadExtensions(extCtx, config.extensions).then((names) => { loadedExtensions = names; }),
|
|
288
243
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
|
|
289
244
|
]).catch((err) => {
|
|
290
245
|
console.error(`Warning: ${err.message}`);
|
|
@@ -292,21 +247,54 @@ async function main() {
|
|
|
292
247
|
if (process.env.DEBUG) {
|
|
293
248
|
console.error('[agent-sh] Extensions loaded');
|
|
294
249
|
}
|
|
295
|
-
// ──
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
250
|
+
// ── Discover skills ───────────────────────────────────────────
|
|
251
|
+
const skills = discoverSkills(process.cwd());
|
|
252
|
+
// ── Activate agent backend ────────────────────────────────────
|
|
253
|
+
// Extensions had their chance to register via agent:register-backend.
|
|
254
|
+
// If none did, the built-in AgentLoop gets wired to bus events.
|
|
255
|
+
core.activateBackend();
|
|
256
|
+
// ── Startup banner ───────────────────────────────────────────
|
|
257
|
+
const settings = getSettings();
|
|
258
|
+
if (settings.startupBanner !== false) {
|
|
259
|
+
const termW = process.stdout.columns || 80;
|
|
260
|
+
const bannerW = Math.min(termW, 60);
|
|
261
|
+
const productName = `${p.accent}${p.bold}agent-sh${p.reset}`;
|
|
262
|
+
const info = agentInfo;
|
|
263
|
+
const backendName = info?.name ?? "agent-sh";
|
|
264
|
+
const model = info?.model ?? core.llmClient?.model;
|
|
265
|
+
const provider = info?.provider;
|
|
266
|
+
const modelValue = model
|
|
267
|
+
? provider ? `${model} [${provider}]` : model
|
|
268
|
+
: null;
|
|
269
|
+
let sections = "";
|
|
270
|
+
sections += `\n\n ${p.muted}Backend:${p.reset} ${p.dim}${backendName}${p.reset}`;
|
|
271
|
+
if (modelValue) {
|
|
272
|
+
sections += `\n ${p.muted}Model:${p.reset} ${p.dim}${modelValue}${p.reset}`;
|
|
273
|
+
}
|
|
274
|
+
if (loadedExtensions.length > 0) {
|
|
275
|
+
sections += `\n\n ${p.muted}Extensions:${p.reset}`;
|
|
276
|
+
for (const name of loadedExtensions) {
|
|
277
|
+
sections += `\n ${p.dim}${name}${p.reset}`;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (skills.length > 0) {
|
|
281
|
+
sections += `\n\n ${p.muted}Skills:${p.reset}`;
|
|
282
|
+
for (const s of skills) {
|
|
283
|
+
sections += `\n ${p.dim}${s.name}${p.reset}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const hint = `${p.muted}Type ${p.warning}>${p.muted} to ask AI · ${p.warning}>/help${p.muted} for commands${p.reset}`;
|
|
287
|
+
const borderLine = `${p.muted}${"─".repeat(bannerW)}${p.reset}`;
|
|
288
|
+
process.stdout.write("\n" + borderLine + "\n" +
|
|
289
|
+
" " + productName +
|
|
290
|
+
sections + "\n" +
|
|
291
|
+
"\n " + hint + "\n" +
|
|
292
|
+
borderLine + "\n\n");
|
|
293
|
+
}
|
|
303
294
|
// ── Terminal lifecycle ────────────────────────────────────────
|
|
304
295
|
process.on("SIGTERM", cleanup);
|
|
305
296
|
process.on("SIGHUP", cleanup);
|
|
306
|
-
// Handle terminal stop/resume signals properly
|
|
307
297
|
process.on("SIGTSTP", () => {
|
|
308
|
-
// Handle Ctrl+Z - suspend the entire process group
|
|
309
|
-
// Restore terminal state before suspending
|
|
310
298
|
if (process.stdin.isTTY) {
|
|
311
299
|
try {
|
|
312
300
|
process.stdin.setRawMode(false);
|
|
@@ -315,14 +303,11 @@ async function main() {
|
|
|
315
303
|
// Ignore
|
|
316
304
|
}
|
|
317
305
|
}
|
|
318
|
-
// Re-send SIGSTOP to actually suspend
|
|
319
306
|
process.kill(process.pid, "SIGSTOP");
|
|
320
307
|
});
|
|
321
308
|
process.on("SIGCONT", () => {
|
|
322
|
-
// Re-acquire terminal when brought back to foreground
|
|
323
309
|
if (process.stdin.isTTY) {
|
|
324
310
|
try {
|
|
325
|
-
// Ensure we reacquire controlling terminal
|
|
326
311
|
process.stdin.setRawMode(true);
|
|
327
312
|
}
|
|
328
313
|
catch {
|
|
@@ -340,17 +325,14 @@ async function main() {
|
|
|
340
325
|
}
|
|
341
326
|
process.exit(e.exitCode);
|
|
342
327
|
});
|
|
343
|
-
// Set up stdin - resume after all event listeners are in place
|
|
344
328
|
if (process.env.DEBUG) {
|
|
345
329
|
console.error('[agent-sh] Resuming stdin...');
|
|
346
330
|
}
|
|
347
331
|
process.stdin.resume();
|
|
348
|
-
// Set raw mode after resume to avoid SIGTTOU issues
|
|
349
332
|
if (process.stdin.isTTY) {
|
|
350
333
|
if (process.env.DEBUG) {
|
|
351
334
|
console.error('[agent-sh] Setting raw mode...');
|
|
352
335
|
}
|
|
353
|
-
// Use setImmediate to ensure we're in the next tick
|
|
354
336
|
setImmediate(() => {
|
|
355
337
|
try {
|
|
356
338
|
process.stdin.setRawMode(true);
|
|
@@ -362,7 +344,6 @@ async function main() {
|
|
|
362
344
|
if (process.env.DEBUG) {
|
|
363
345
|
console.error(`[agent-sh] Failed to set raw mode: ${err}`);
|
|
364
346
|
}
|
|
365
|
-
// May fail if process is in background; SIGTTOU handler prevents suspension
|
|
366
347
|
}
|
|
367
348
|
});
|
|
368
349
|
}
|
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;
|
|
@@ -442,7 +509,8 @@ export class InputHandler {
|
|
|
442
509
|
}
|
|
443
510
|
this.editor.buffer = this.history[this.historyIndex];
|
|
444
511
|
this.editor.cursor = this.editor.buffer.length;
|
|
445
|
-
this.
|
|
512
|
+
this.clearAutocompleteLines();
|
|
513
|
+
this.writeModePromptLine();
|
|
446
514
|
}
|
|
447
515
|
break;
|
|
448
516
|
case "arrow-down":
|
|
@@ -465,7 +533,8 @@ export class InputHandler {
|
|
|
465
533
|
this.editor.buffer = this.savedBuffer;
|
|
466
534
|
}
|
|
467
535
|
this.editor.cursor = this.editor.buffer.length;
|
|
468
|
-
this.
|
|
536
|
+
this.clearAutocompleteLines();
|
|
537
|
+
this.writeModePromptLine();
|
|
469
538
|
}
|
|
470
539
|
break;
|
|
471
540
|
}
|
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]) {
|