agent-sh 0.10.3 → 0.12.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 +42 -8
- package/dist/agent/agent-loop.js +87 -69
- package/dist/agent/conversation-state.d.ts +8 -1
- package/dist/agent/conversation-state.js +35 -14
- package/dist/agent/subagent.d.ts +8 -4
- package/dist/agent/subagent.js +45 -5
- package/dist/agent/tool-protocol.d.ts +5 -5
- package/dist/agent/tool-protocol.js +8 -8
- package/dist/core.d.ts +1 -1
- package/dist/core.js +2 -0
- package/dist/event-bus.d.ts +12 -1
- package/dist/extensions/agent-backend.js +46 -28
- package/dist/extensions/index.d.ts +4 -2
- package/dist/extensions/index.js +11 -3
- package/dist/extensions/openai.d.ts +7 -0
- package/dist/extensions/openai.js +46 -0
- package/dist/extensions/openrouter.d.ts +7 -0
- package/dist/extensions/openrouter.js +39 -0
- package/dist/extensions/tui-renderer.js +65 -19
- package/dist/index.js +24 -3
- package/dist/init.d.ts +3 -0
- package/dist/init.js +72 -0
- package/dist/shell/input-handler.js +30 -0
- package/dist/shell/shell.d.ts +2 -5
- package/dist/shell/shell.js +20 -17
- package/dist/types.d.ts +26 -0
- package/dist/utils/llm-facade.d.ts +7 -0
- package/dist/utils/llm-facade.js +33 -0
- package/examples/extensions/wire-log.ts +35 -0
- package/package.json +2 -2
- package/dist/extensions/command-suggest.d.ts +0 -10
- package/dist/extensions/command-suggest.js +0 -42
package/dist/shell/shell.d.ts
CHANGED
|
@@ -34,11 +34,8 @@ export declare class Shell implements InputContext {
|
|
|
34
34
|
isAgentActive(): boolean;
|
|
35
35
|
writeToPty(data: string): void;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* ZLE widget
|
|
39
|
-
* prompt correctly — we don't try to replay captured bytes.
|
|
40
|
-
*
|
|
41
|
-
* For bash, falls back to sending \n for a fresh prompt cycle.
|
|
37
|
+
* Ask the shell to redraw its own prompt in place via \e[9999~, which both
|
|
38
|
+
* zsh (ZLE widget) and bash (readline redraw-current-line) bind to repaint.
|
|
42
39
|
*/
|
|
43
40
|
redrawPrompt(): void;
|
|
44
41
|
/**
|
package/dist/shell/shell.js
CHANGED
|
@@ -87,7 +87,15 @@ export class Shell {
|
|
|
87
87
|
`[ -f "${userHome}/.bashrc" ] && source "${userHome}/.bashrc"`,
|
|
88
88
|
"",
|
|
89
89
|
"# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
|
|
90
|
-
|
|
90
|
+
"# Wrapped in a function because inlining printf \"...\" into",
|
|
91
|
+
"# PROMPT_COMMAND=\"...\" breaks the outer quoting.",
|
|
92
|
+
"__agent_sh_precmd() {",
|
|
93
|
+
` ${osc7Cmd}`,
|
|
94
|
+
` ${promptMarker}`,
|
|
95
|
+
...(showIndicator ? [` ${titleCmd}`] : []),
|
|
96
|
+
" __agent_sh_preexec_ran=0",
|
|
97
|
+
"}",
|
|
98
|
+
`PROMPT_COMMAND="\${PROMPT_COMMAND:+\$PROMPT_COMMAND;}__agent_sh_precmd"`,
|
|
91
99
|
"",
|
|
92
100
|
"# Preexec hook via DEBUG trap: emit actual command text so agent-sh",
|
|
93
101
|
"# can track history-recalled and tab-completed commands accurately",
|
|
@@ -104,6 +112,12 @@ export class Shell {
|
|
|
104
112
|
"",
|
|
105
113
|
"# End-of-prompt marker: append to PS1 (\\[...\\] marks it zero-width)",
|
|
106
114
|
'case "$PS1" in *9998*) ;; *) PS1="${PS1}\\[\\e]9998;READY\\a\\]";; esac',
|
|
115
|
+
"",
|
|
116
|
+
"# Mirrors the zsh \\e[9999~ reset-prompt widget — used by agent-sh",
|
|
117
|
+
"# to repaint the prompt in place. All keymaps so `set -o vi` works.",
|
|
118
|
+
`bind -m emacs '"\\e[9999~":redraw-current-line' 2>/dev/null`,
|
|
119
|
+
`bind -m vi-insert '"\\e[9999~":redraw-current-line' 2>/dev/null`,
|
|
120
|
+
`bind -m vi-command '"\\e[9999~":redraw-current-line' 2>/dev/null`,
|
|
107
121
|
];
|
|
108
122
|
fs.writeFileSync(path.join(this.tmpDir, ".bashrc"), bashrcLines.join("\n") + "\n");
|
|
109
123
|
shellArgs = ["--rcfile", path.join(this.tmpDir, ".bashrc")];
|
|
@@ -190,16 +204,12 @@ export class Shell {
|
|
|
190
204
|
this.ptyProcess.write(data);
|
|
191
205
|
}
|
|
192
206
|
/**
|
|
193
|
-
*
|
|
194
|
-
* ZLE widget
|
|
195
|
-
* prompt correctly — we don't try to replay captured bytes.
|
|
196
|
-
*
|
|
197
|
-
* For bash, falls back to sending \n for a fresh prompt cycle.
|
|
207
|
+
* Ask the shell to redraw its own prompt in place via \e[9999~, which both
|
|
208
|
+
* zsh (ZLE widget) and bash (readline redraw-current-line) bind to repaint.
|
|
198
209
|
*/
|
|
199
210
|
redrawPrompt() {
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// terminal appear frozen. Reset both before emitting.
|
|
211
|
+
// Stale echoSkip/paused from handleProcessingDone re-entering a mode
|
|
212
|
+
// would swallow the redraw and freeze the terminal visually.
|
|
203
213
|
this.echoSkip = false;
|
|
204
214
|
this.paused = false;
|
|
205
215
|
const result = this.bus.emitPipe("shell:redraw-prompt", {
|
|
@@ -207,14 +217,7 @@ export class Shell {
|
|
|
207
217
|
handled: false,
|
|
208
218
|
});
|
|
209
219
|
if (!result.handled) {
|
|
210
|
-
|
|
211
|
-
// Trigger the hidden ZLE widget — zle reset-prompt redraws cleanly
|
|
212
|
-
this.ptyProcess.write("\x1b[9999~");
|
|
213
|
-
}
|
|
214
|
-
else {
|
|
215
|
-
// Bash: no zle reset-prompt equivalent, use fresh prompt cycle
|
|
216
|
-
this.ptyProcess.write("\n");
|
|
217
|
-
}
|
|
220
|
+
this.ptyProcess.write("\x1b[9999~");
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
223
|
/**
|
package/dist/types.d.ts
CHANGED
|
@@ -49,6 +49,31 @@ export interface AgentMode {
|
|
|
49
49
|
/** Provider supports the reasoning_effort parameter. */
|
|
50
50
|
supportsReasoningEffort?: boolean;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Backend-agnostic LLM interface exposed via `ctx.llm`. Backends fulfill it
|
|
54
|
+
* by defining an `llm:invoke` handler; those without an LLM leave
|
|
55
|
+
* `available` false and calls reject.
|
|
56
|
+
*/
|
|
57
|
+
export interface LlmMessage {
|
|
58
|
+
role: "system" | "user" | "assistant";
|
|
59
|
+
content: string;
|
|
60
|
+
}
|
|
61
|
+
export interface LlmSession {
|
|
62
|
+
send(message: string): Promise<string>;
|
|
63
|
+
history(): ReadonlyArray<LlmMessage>;
|
|
64
|
+
}
|
|
65
|
+
export interface LlmInterface {
|
|
66
|
+
readonly available: boolean;
|
|
67
|
+
ask(opts: {
|
|
68
|
+
query: string;
|
|
69
|
+
system?: string;
|
|
70
|
+
maxTokens?: number;
|
|
71
|
+
}): Promise<string>;
|
|
72
|
+
session(opts?: {
|
|
73
|
+
system?: string;
|
|
74
|
+
maxTokens?: number;
|
|
75
|
+
}): LlmSession;
|
|
76
|
+
}
|
|
52
77
|
export interface AgentShellConfig {
|
|
53
78
|
shell?: string;
|
|
54
79
|
model?: string;
|
|
@@ -102,6 +127,7 @@ export interface ExtensionContext {
|
|
|
102
127
|
registerSkill: (name: string, description: string, filePath: string) => void;
|
|
103
128
|
/** Remove a registered skill by name. */
|
|
104
129
|
removeSkill: (name: string) => void;
|
|
130
|
+
llm: LlmInterface;
|
|
105
131
|
/** Register a named handler. */
|
|
106
132
|
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
107
133
|
/** Wrap a named handler. Receives `next` (original) + args. Returns an unadvise function. */
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ctx.llm facade — delegates to an `llm:invoke` handler registered by the
|
|
3
|
+
* active backend. No handler → `available` is false and calls reject.
|
|
4
|
+
*/
|
|
5
|
+
import type { HandlerRegistry } from "./handler-registry.js";
|
|
6
|
+
import type { LlmInterface } from "../types.js";
|
|
7
|
+
export declare function createLlmFacade(handlers: HandlerRegistry): LlmInterface;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function createLlmFacade(handlers) {
|
|
2
|
+
const invoke = (messages, maxTokens) => {
|
|
3
|
+
const result = handlers.call("llm:invoke", messages, { maxTokens });
|
|
4
|
+
if (result === undefined)
|
|
5
|
+
return Promise.reject(new Error("ctx.llm: no LLM backend available"));
|
|
6
|
+
return result;
|
|
7
|
+
};
|
|
8
|
+
return {
|
|
9
|
+
get available() { return handlers.list().includes("llm:invoke"); },
|
|
10
|
+
ask: ({ query, system, maxTokens }) => {
|
|
11
|
+
const messages = [];
|
|
12
|
+
if (system)
|
|
13
|
+
messages.push({ role: "system", content: system });
|
|
14
|
+
messages.push({ role: "user", content: query });
|
|
15
|
+
return invoke(messages, maxTokens);
|
|
16
|
+
},
|
|
17
|
+
session: (opts = {}) => {
|
|
18
|
+
const messages = [];
|
|
19
|
+
if (opts.system)
|
|
20
|
+
messages.push({ role: "system", content: opts.system });
|
|
21
|
+
const session = {
|
|
22
|
+
async send(message) {
|
|
23
|
+
messages.push({ role: "user", content: message });
|
|
24
|
+
const reply = await invoke(messages, opts.maxTokens);
|
|
25
|
+
messages.push({ role: "assistant", content: reply });
|
|
26
|
+
return reply;
|
|
27
|
+
},
|
|
28
|
+
history: () => messages.slice(),
|
|
29
|
+
};
|
|
30
|
+
return session;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dumps every LLM request + streamed chunk to $AGENT_SH_WIRE_DIR
|
|
3
|
+
* (default ~/.agent-sh/wire) for offline replay via curl. Paired files
|
|
4
|
+
* per turn: <stamp>.request.json and <stamp>.chunks.jsonl.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
10
|
+
|
|
11
|
+
export default function activate(ctx: ExtensionContext): void {
|
|
12
|
+
const dir = process.env.AGENT_SH_WIRE_DIR
|
|
13
|
+
?? path.join(os.homedir(), ".agent-sh", "wire");
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
// llm:chunk has no back-pointer to its request, so anchor both on
|
|
17
|
+
// the timestamp set when llm:request fires.
|
|
18
|
+
let currentStamp: string | null = null;
|
|
19
|
+
|
|
20
|
+
ctx.bus.on("llm:request", (req) => {
|
|
21
|
+
currentStamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(dir, `${currentStamp}.request.json`),
|
|
24
|
+
JSON.stringify(req, null, 2),
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
ctx.bus.on("llm:chunk", ({ chunk }) => {
|
|
29
|
+
if (!currentStamp) return;
|
|
30
|
+
fs.appendFileSync(
|
|
31
|
+
path.join(dir, `${currentStamp}.chunks.jsonl`),
|
|
32
|
+
JSON.stringify(chunk) + "\n",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A shell-first terminal where AI is one keystroke away",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/core.js",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"dev": "tsx src/index.ts",
|
|
96
96
|
"build": "tsc",
|
|
97
97
|
"start": "node dist/index.js",
|
|
98
|
-
"
|
|
98
|
+
"prepare": "npm run build"
|
|
99
99
|
},
|
|
100
100
|
"keywords": [
|
|
101
101
|
"terminal",
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Command suggestion extension (fast-path LLM feature).
|
|
3
|
-
*
|
|
4
|
-
* After a shell command fails (non-zero exit), uses LlmClient.complete()
|
|
5
|
-
* to suggest a fix. Shows the suggestion below the prompt.
|
|
6
|
-
*
|
|
7
|
-
* Only active when an LLM client is available (registered by agent-backend).
|
|
8
|
-
*/
|
|
9
|
-
import type { ExtensionContext } from "../types.js";
|
|
10
|
-
export default function activate({ bus, call }: ExtensionContext): void;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export default function activate({ bus, call }) {
|
|
2
|
-
let suggesting = false;
|
|
3
|
-
bus.on("shell:command-done", ({ command, output, exitCode, cwd }) => {
|
|
4
|
-
if (exitCode === null || exitCode === 0)
|
|
5
|
-
return;
|
|
6
|
-
if (!command.trim())
|
|
7
|
-
return;
|
|
8
|
-
if (suggesting)
|
|
9
|
-
return; // don't stack suggestions
|
|
10
|
-
const llmClient = call("llm:get-client");
|
|
11
|
-
if (!llmClient)
|
|
12
|
-
return;
|
|
13
|
-
suggesting = true;
|
|
14
|
-
// Truncate output to avoid blowing up the prompt
|
|
15
|
-
const truncated = output.length > 1000
|
|
16
|
-
? output.slice(-1000)
|
|
17
|
-
: output;
|
|
18
|
-
llmClient.complete({
|
|
19
|
-
messages: [
|
|
20
|
-
{
|
|
21
|
-
role: "system",
|
|
22
|
-
content: "You are a shell assistant. The user's command failed. " +
|
|
23
|
-
"Suggest a fix as a single command. Just the command, no explanation, no backticks, no prefix. " +
|
|
24
|
-
"If you can't suggest anything useful, reply with an empty string.",
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
role: "user",
|
|
28
|
-
content: `cwd: ${cwd}\n$ ${command}\n${truncated}\nexit code: ${exitCode}`,
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
max_tokens: 150,
|
|
32
|
-
}).then((suggestion) => {
|
|
33
|
-
suggesting = false;
|
|
34
|
-
const trimmed = suggestion.trim().replace(/^`+|`+$/g, ""); // strip backticks
|
|
35
|
-
if (trimmed && trimmed.length < 500) {
|
|
36
|
-
bus.emit("ui:suggestion", { text: trimmed });
|
|
37
|
-
}
|
|
38
|
-
}).catch(() => {
|
|
39
|
-
suggesting = false;
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|