agent-sh 0.3.1 → 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 -96
- 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 +84 -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 +111 -53
- package/dist/index.js +124 -120
- package/dist/input-handler.d.ts +17 -8
- package/dist/input-handler.js +152 -45
- 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 +36 -27
- package/dist/types.d.ts +46 -6
- package/dist/utils/box-frame.d.ts +3 -1
- package/dist/utils/box-frame.js +12 -5
- package/dist/utils/line-editor.js +4 -0
- 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 -100
- package/dist/acp-client.js +0 -656
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import * as path from "node:path";
|
|
3
4
|
import { Shell } from "./shell.js";
|
|
4
5
|
import { createCore } from "./core.js";
|
|
5
6
|
import { palette as p } from "./utils/palette.js";
|
|
@@ -7,19 +8,23 @@ import tuiRenderer from "./extensions/tui-renderer.js";
|
|
|
7
8
|
import slashCommands from "./extensions/slash-commands.js";
|
|
8
9
|
import fileAutocomplete from "./extensions/file-autocomplete.js";
|
|
9
10
|
import shellRecall from "./extensions/shell-recall.js";
|
|
10
|
-
import
|
|
11
|
+
import commandSuggest from "./extensions/command-suggest.js";
|
|
11
12
|
import { loadExtensions } from "./extension-loader.js";
|
|
13
|
+
import { getSettings } from "./settings.js";
|
|
12
14
|
/**
|
|
13
|
-
* Capture the user's full shell environment
|
|
15
|
+
* Capture the user's full shell environment.
|
|
14
16
|
* This picks up env vars exported in .zshrc/.bashrc that the
|
|
15
|
-
* Node.js process doesn't have.
|
|
16
|
-
*
|
|
17
|
-
* Uses -l (login shell) instead of -i to avoid TTY blocking issues.
|
|
17
|
+
* Node.js process doesn't have (e.g. when launched from an IDE).
|
|
18
18
|
*/
|
|
19
19
|
async function captureShellEnvAsync(shell) {
|
|
20
20
|
return new Promise((resolve) => {
|
|
21
21
|
try {
|
|
22
|
-
const
|
|
22
|
+
const shellName = path.basename(shell);
|
|
23
|
+
const isZsh = shellName.includes("zsh");
|
|
24
|
+
const sourceRc = isZsh
|
|
25
|
+
? 'source ~/.zshrc 2>/dev/null;'
|
|
26
|
+
: '[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null;';
|
|
27
|
+
const child = spawn(shell, ["-l", "-c", `${sourceRc} env -0`], {
|
|
23
28
|
stdio: ["ignore", "pipe", "ignore"],
|
|
24
29
|
timeout: 5000,
|
|
25
30
|
});
|
|
@@ -29,7 +34,7 @@ async function captureShellEnvAsync(shell) {
|
|
|
29
34
|
});
|
|
30
35
|
child.on("close", (code) => {
|
|
31
36
|
if (code !== 0 || !output) {
|
|
32
|
-
resolve({});
|
|
37
|
+
resolve({});
|
|
33
38
|
return;
|
|
34
39
|
}
|
|
35
40
|
const env = {};
|
|
@@ -41,9 +46,8 @@ async function captureShellEnvAsync(shell) {
|
|
|
41
46
|
resolve(env);
|
|
42
47
|
});
|
|
43
48
|
child.on("error", () => {
|
|
44
|
-
resolve({});
|
|
49
|
+
resolve({});
|
|
45
50
|
});
|
|
46
|
-
// Safety timeout
|
|
47
51
|
setTimeout(() => {
|
|
48
52
|
child.kill("SIGTERM");
|
|
49
53
|
resolve({});
|
|
@@ -54,14 +58,9 @@ async function captureShellEnvAsync(shell) {
|
|
|
54
58
|
}
|
|
55
59
|
});
|
|
56
60
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Merge captured shell env into base env, only adding keys that don't exist.
|
|
59
|
-
* This preserves any runtime modifications while adding missing shell vars.
|
|
60
|
-
*/
|
|
61
61
|
function mergeShellEnv(baseEnv, shellEnv) {
|
|
62
62
|
const merged = { ...baseEnv };
|
|
63
63
|
for (const [key, value] of Object.entries(shellEnv)) {
|
|
64
|
-
// Only add if key doesn't exist or is empty in base env
|
|
65
64
|
if (!(key in merged) || !merged[key]) {
|
|
66
65
|
merged[key] = value;
|
|
67
66
|
}
|
|
@@ -69,127 +68,110 @@ function mergeShellEnv(baseEnv, shellEnv) {
|
|
|
69
68
|
return merged;
|
|
70
69
|
}
|
|
71
70
|
function parseArgs(argv) {
|
|
72
|
-
// Priority: CLI args > Environment variables > Config file > Defaults
|
|
73
|
-
const defaultAgent = process.env.AGENT_SH_AGENT || "pi-acp";
|
|
74
|
-
let agentCommand = defaultAgent;
|
|
75
|
-
let agentArgs = [];
|
|
76
71
|
let model;
|
|
77
72
|
let extensions;
|
|
73
|
+
let provider;
|
|
78
74
|
const shell = process.env.SHELL || "/bin/bash";
|
|
75
|
+
let apiKey = process.env.OPENAI_API_KEY;
|
|
76
|
+
let baseURL = process.env.OPENAI_BASE_URL;
|
|
79
77
|
for (let i = 0; i < argv.length; i++) {
|
|
80
78
|
const arg = argv[i];
|
|
81
|
-
if (arg === "--
|
|
82
|
-
|
|
79
|
+
if (arg === "--model" && argv[i + 1]) {
|
|
80
|
+
model = argv[++i];
|
|
83
81
|
}
|
|
84
|
-
else if (arg === "--
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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];
|
|
92
90
|
}
|
|
93
91
|
else if (arg === "--shell" && argv[i + 1]) {
|
|
94
|
-
return {
|
|
92
|
+
return { shell: argv[++i], model, extensions, apiKey, baseURL, provider };
|
|
95
93
|
}
|
|
96
94
|
else if ((arg === "--extensions" || arg === "-e") && argv[i + 1]) {
|
|
97
95
|
const exts = argv[++i].split(",").map(s => s.trim());
|
|
98
96
|
extensions = extensions ? [...extensions, ...exts] : exts;
|
|
99
97
|
}
|
|
100
98
|
else if (arg === "--help" || arg === "-h") {
|
|
101
|
-
console.log(`agent-sh — a shell-first terminal
|
|
99
|
+
console.log(`agent-sh — a shell-first terminal where AI is one keystroke away
|
|
102
100
|
|
|
103
101
|
Usage: agent-sh [options]
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
npm run claude Start with Claude agent
|
|
103
|
+
Provider Profiles:
|
|
104
|
+
--provider <name> Use a provider from ~/.agent-sh/settings.json
|
|
105
|
+
--model <name> Override default model
|
|
109
106
|
|
|
110
|
-
|
|
111
|
-
--
|
|
112
|
-
--
|
|
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)
|
|
110
|
+
|
|
111
|
+
General Options:
|
|
113
112
|
--shell <path> Shell to use (default: $SHELL or /bin/bash)
|
|
114
113
|
-e, --extensions Extensions to load (comma-separated, repeatable)
|
|
115
114
|
-h, --help Show this help
|
|
116
115
|
|
|
117
|
-
Extensions:
|
|
118
|
-
Extensions are loaded from (in order):
|
|
119
|
-
1. -e flags: npm packages or file paths
|
|
120
|
-
2. settings: ~/.agent-sh/settings.json → "extensions": [...]
|
|
121
|
-
3. directory: ~/.agent-sh/extensions/ (files or dirs with index.ts)
|
|
122
|
-
|
|
123
116
|
Environment Variables:
|
|
124
|
-
|
|
117
|
+
OPENAI_API_KEY API key for LLM provider
|
|
118
|
+
OPENAI_BASE_URL Base URL override (e.g., http://localhost:11434/v1 for Ollama)
|
|
125
119
|
|
|
126
120
|
Examples:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
130
129
|
|
|
131
130
|
Inside the shell:
|
|
132
131
|
Type normally Commands run in your real shell
|
|
133
|
-
> <query>
|
|
132
|
+
> <query> Ask the AI agent a question (execute mode)
|
|
133
|
+
? <command> Have the agent run a command in your shell (help mode)
|
|
134
134
|
> /help Show available slash commands
|
|
135
135
|
Ctrl-C Cancel agent response (or signal shell as usual)
|
|
136
136
|
`);
|
|
137
137
|
process.exit(0);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
return {
|
|
141
|
-
}
|
|
142
|
-
function formatAgentInfo(agentInfo, model, thoughtLevel) {
|
|
143
|
-
const name = agentInfo.name.replace(/-acp$/, "").replace(/-/g, " ");
|
|
144
|
-
let infoStr = `${p.dim}${name}${p.reset}`;
|
|
145
|
-
if (model) {
|
|
146
|
-
const cleanModel = model
|
|
147
|
-
.replace(/^openai\//i, "")
|
|
148
|
-
.replace(/^anthropic\//i, "")
|
|
149
|
-
.replace(/^google\//i, "");
|
|
150
|
-
infoStr += ` ${p.dim}(${cleanModel})${p.reset}`;
|
|
151
|
-
}
|
|
152
|
-
if (thoughtLevel) {
|
|
153
|
-
// Clean up verbose mode names like "Thinking: medium" → "medium"
|
|
154
|
-
const label = thoughtLevel.replace(/^Thinking:\s*/i, "");
|
|
155
|
-
infoStr += ` ${p.dim}[${label}]${p.reset}`;
|
|
156
|
-
}
|
|
157
|
-
return `${infoStr} ${p.success}●${p.reset}`;
|
|
140
|
+
return { shell, model, extensions, apiKey, baseURL, provider };
|
|
158
141
|
}
|
|
159
142
|
async function main() {
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
}
|
|
162
147
|
process.on("SIGTTOU", () => { });
|
|
163
|
-
// Also ignore SIGTTIN which can occur when reading from terminal while backgrounded.
|
|
164
148
|
process.on("SIGTTIN", () => { });
|
|
165
149
|
const config = parseArgs(process.argv.slice(2));
|
|
166
|
-
//
|
|
167
|
-
// We'll enrich it with shell env asynchronously in the background
|
|
150
|
+
// Capture user's full shell environment
|
|
168
151
|
const baseEnv = {};
|
|
169
152
|
for (const [k, v] of Object.entries(process.env)) {
|
|
170
153
|
if (v !== undefined)
|
|
171
154
|
baseEnv[k] = v;
|
|
172
155
|
}
|
|
173
|
-
config.shellEnv = baseEnv;
|
|
174
|
-
// Asynchronously capture full shell environment without blocking startup
|
|
175
156
|
const shellPath = config.shell || process.env.SHELL || "/bin/bash";
|
|
176
|
-
|
|
157
|
+
try {
|
|
158
|
+
const shellEnv = await captureShellEnvAsync(shellPath);
|
|
177
159
|
if (Object.keys(shellEnv).length > 0) {
|
|
178
|
-
|
|
179
|
-
config.shellEnv = merged;
|
|
160
|
+
Object.assign(baseEnv, mergeShellEnv(baseEnv, shellEnv));
|
|
180
161
|
if (process.env.DEBUG) {
|
|
181
|
-
console.error('[agent-sh] Shell environment
|
|
162
|
+
console.error('[agent-sh] Shell environment captured');
|
|
182
163
|
}
|
|
183
164
|
}
|
|
184
|
-
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
185
167
|
// Ignore errors, we already have process.env as fallback
|
|
186
|
-
});
|
|
187
|
-
if (process.env.DEBUG) {
|
|
188
|
-
console.error('[agent-sh] Using current process environment (async enrichment pending)');
|
|
189
168
|
}
|
|
190
169
|
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
191
170
|
const core = createCore(config);
|
|
192
|
-
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; });
|
|
193
175
|
// ── Interactive frontend ──────────────────────────────────────
|
|
194
176
|
if (process.env.DEBUG) {
|
|
195
177
|
console.error('[agent-sh] Setting up interactive frontend...');
|
|
@@ -208,8 +190,6 @@ async function main() {
|
|
|
208
190
|
if (process.env.DEBUG) {
|
|
209
191
|
console.error('[agent-sh] Creating Shell...');
|
|
210
192
|
}
|
|
211
|
-
// Small delay on macOS to ensure we're fully in the foreground process group
|
|
212
|
-
// before spawning the PTY. This prevents SIGTTOU suspension.
|
|
213
193
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
214
194
|
const shell = new Shell({
|
|
215
195
|
bus,
|
|
@@ -218,13 +198,11 @@ async function main() {
|
|
|
218
198
|
shell: config.shell || process.env.SHELL || "/bin/bash",
|
|
219
199
|
cwd: process.cwd(),
|
|
220
200
|
onShowAgentInfo: () => {
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return { info: formatAgentInfo(agentInfo, model, mode?.name ?? null) };
|
|
227
|
-
}
|
|
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}` };
|
|
228
206
|
}
|
|
229
207
|
return { info: "" };
|
|
230
208
|
},
|
|
@@ -232,6 +210,40 @@ async function main() {
|
|
|
232
210
|
if (process.env.DEBUG) {
|
|
233
211
|
console.error('[agent-sh] Shell created');
|
|
234
212
|
}
|
|
213
|
+
// ── Input modes ──────────────────────────────────────────────
|
|
214
|
+
bus.emit("input-mode:register", {
|
|
215
|
+
id: "execute",
|
|
216
|
+
trigger: ">",
|
|
217
|
+
label: "execute",
|
|
218
|
+
promptIcon: "❯",
|
|
219
|
+
indicator: "●",
|
|
220
|
+
onSubmit(query, b) {
|
|
221
|
+
b.emit("agent:submit", { query, modeLabel: "Execute", modeInstruction: "[mode: execute]" });
|
|
222
|
+
},
|
|
223
|
+
returnToSelf: true,
|
|
224
|
+
});
|
|
225
|
+
bus.emit("input-mode:register", {
|
|
226
|
+
id: "help",
|
|
227
|
+
trigger: "?",
|
|
228
|
+
label: "help",
|
|
229
|
+
promptIcon: "❯",
|
|
230
|
+
indicator: "❓",
|
|
231
|
+
onSubmit(query, b) {
|
|
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]" });
|
|
244
|
+
},
|
|
245
|
+
returnToSelf: false,
|
|
246
|
+
});
|
|
235
247
|
// ── Extensions ────────────────────────────────────────────────
|
|
236
248
|
if (process.env.DEBUG) {
|
|
237
249
|
console.error('[agent-sh] Setting up extensions...');
|
|
@@ -241,20 +253,12 @@ async function main() {
|
|
|
241
253
|
slashCommands(extCtx);
|
|
242
254
|
fileAutocomplete(extCtx);
|
|
243
255
|
shellRecall(extCtx);
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
const tmpDir = shell.getTmpDir();
|
|
247
|
-
if (tmpDir) {
|
|
248
|
-
if (process.env.DEBUG) {
|
|
249
|
-
console.error('[agent-sh] Starting shell-exec socket server...');
|
|
250
|
-
}
|
|
251
|
-
shellExec(extCtx, { socketPath: `${tmpDir}/shell.sock` });
|
|
252
|
-
}
|
|
253
|
-
// Load extensions with timeout to prevent blocking startup
|
|
256
|
+
commandSuggest(extCtx);
|
|
257
|
+
// Load user extensions (may register alternative agent backends)
|
|
254
258
|
if (process.env.DEBUG) {
|
|
255
259
|
console.error('[agent-sh] Loading extensions...');
|
|
256
260
|
}
|
|
257
|
-
const loadExtensionsTimeoutMs = 10000;
|
|
261
|
+
const loadExtensionsTimeoutMs = 10000;
|
|
258
262
|
await Promise.race([
|
|
259
263
|
loadExtensions(extCtx, config.extensions),
|
|
260
264
|
new Promise((_, reject) => setTimeout(() => reject(new Error(`Extension loading timeout after ${loadExtensionsTimeoutMs}ms`)), loadExtensionsTimeoutMs)),
|
|
@@ -264,21 +268,28 @@ async function main() {
|
|
|
264
268
|
if (process.env.DEBUG) {
|
|
265
269
|
console.error('[agent-sh] Extensions loaded');
|
|
266
270
|
}
|
|
267
|
-
// ──
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
}
|
|
275
289
|
// ── Terminal lifecycle ────────────────────────────────────────
|
|
276
290
|
process.on("SIGTERM", cleanup);
|
|
277
291
|
process.on("SIGHUP", cleanup);
|
|
278
|
-
// Handle terminal stop/resume signals properly
|
|
279
292
|
process.on("SIGTSTP", () => {
|
|
280
|
-
// Handle Ctrl+Z - suspend the entire process group
|
|
281
|
-
// Restore terminal state before suspending
|
|
282
293
|
if (process.stdin.isTTY) {
|
|
283
294
|
try {
|
|
284
295
|
process.stdin.setRawMode(false);
|
|
@@ -287,14 +298,11 @@ async function main() {
|
|
|
287
298
|
// Ignore
|
|
288
299
|
}
|
|
289
300
|
}
|
|
290
|
-
// Re-send SIGSTOP to actually suspend
|
|
291
301
|
process.kill(process.pid, "SIGSTOP");
|
|
292
302
|
});
|
|
293
303
|
process.on("SIGCONT", () => {
|
|
294
|
-
// Re-acquire terminal when brought back to foreground
|
|
295
304
|
if (process.stdin.isTTY) {
|
|
296
305
|
try {
|
|
297
|
-
// Ensure we reacquire controlling terminal
|
|
298
306
|
process.stdin.setRawMode(true);
|
|
299
307
|
}
|
|
300
308
|
catch {
|
|
@@ -312,17 +320,14 @@ async function main() {
|
|
|
312
320
|
}
|
|
313
321
|
process.exit(e.exitCode);
|
|
314
322
|
});
|
|
315
|
-
// Set up stdin - resume after all event listeners are in place
|
|
316
323
|
if (process.env.DEBUG) {
|
|
317
324
|
console.error('[agent-sh] Resuming stdin...');
|
|
318
325
|
}
|
|
319
326
|
process.stdin.resume();
|
|
320
|
-
// Set raw mode after resume to avoid SIGTTOU issues
|
|
321
327
|
if (process.stdin.isTTY) {
|
|
322
328
|
if (process.env.DEBUG) {
|
|
323
329
|
console.error('[agent-sh] Setting raw mode...');
|
|
324
330
|
}
|
|
325
|
-
// Use setImmediate to ensure we're in the next tick
|
|
326
331
|
setImmediate(() => {
|
|
327
332
|
try {
|
|
328
333
|
process.stdin.setRawMode(true);
|
|
@@ -334,7 +339,6 @@ async function main() {
|
|
|
334
339
|
if (process.env.DEBUG) {
|
|
335
340
|
console.error(`[agent-sh] Failed to set raw mode: ${err}`);
|
|
336
341
|
}
|
|
337
|
-
// May fail if process is in background; SIGTTOU handler prevents suspension
|
|
338
342
|
}
|
|
339
343
|
});
|
|
340
344
|
}
|
package/dist/input-handler.d.ts
CHANGED
|
@@ -16,7 +16,10 @@ export interface InputContext {
|
|
|
16
16
|
export declare class InputHandler {
|
|
17
17
|
private ctx;
|
|
18
18
|
private lineBuffer;
|
|
19
|
-
private
|
|
19
|
+
private activeMode;
|
|
20
|
+
private pendingReturnMode;
|
|
21
|
+
private modes;
|
|
22
|
+
private modesById;
|
|
20
23
|
private editor;
|
|
21
24
|
private autocompleteActive;
|
|
22
25
|
private autocompleteIndex;
|
|
@@ -37,22 +40,28 @@ export declare class InputHandler {
|
|
|
37
40
|
model?: string;
|
|
38
41
|
};
|
|
39
42
|
});
|
|
43
|
+
private registerMode;
|
|
40
44
|
private loadHistory;
|
|
41
45
|
private saveHistory;
|
|
42
|
-
/** Write the
|
|
43
|
-
private
|
|
46
|
+
/** Write the mode prompt line with cursor at the correct position. */
|
|
47
|
+
private writeModePromptLine;
|
|
44
48
|
handleInput(data: string): void;
|
|
45
|
-
private
|
|
46
|
-
private
|
|
49
|
+
private enterMode;
|
|
50
|
+
private exitMode;
|
|
47
51
|
/** Move to the start of the prompt area and clear everything below. */
|
|
48
52
|
private clearPromptArea;
|
|
49
53
|
printPrompt(): void;
|
|
50
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Called when agent processing completes. Returns true if the input
|
|
56
|
+
* handler re-entered a mode (so caller should skip shell prompt).
|
|
57
|
+
*/
|
|
58
|
+
handleProcessingDone(): boolean;
|
|
59
|
+
private renderModeInput;
|
|
51
60
|
private updateAutocomplete;
|
|
52
61
|
private renderAutocomplete;
|
|
53
62
|
private applyAutocomplete;
|
|
54
63
|
private dismissAutocomplete;
|
|
55
64
|
private clearAutocompleteLines;
|
|
56
|
-
private
|
|
57
|
-
private
|
|
65
|
+
private handleModeInput;
|
|
66
|
+
private processModeActions;
|
|
58
67
|
}
|