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/settings.js
CHANGED
|
@@ -12,6 +12,9 @@ const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
|
|
|
12
12
|
const DEFAULTS = {
|
|
13
13
|
extensions: [],
|
|
14
14
|
historySize: 500,
|
|
15
|
+
providers: {},
|
|
16
|
+
defaultProvider: undefined,
|
|
17
|
+
defaultBackend: "agent-sh",
|
|
15
18
|
contextWindowSize: 20,
|
|
16
19
|
contextBudget: 16384,
|
|
17
20
|
shellTruncateThreshold: 10,
|
|
@@ -21,7 +24,9 @@ const DEFAULTS = {
|
|
|
21
24
|
maxCommandOutputLines: 3,
|
|
22
25
|
readOutputMaxLines: 0,
|
|
23
26
|
diffMaxLines: 20,
|
|
24
|
-
|
|
27
|
+
skillPaths: [],
|
|
28
|
+
startupBanner: true,
|
|
29
|
+
promptIndicator: true,
|
|
25
30
|
};
|
|
26
31
|
let cached = null;
|
|
27
32
|
/** Load settings from disk (cached after first call). */
|
|
@@ -31,7 +36,10 @@ export function getSettings() {
|
|
|
31
36
|
const raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
|
|
32
37
|
cached = JSON.parse(raw);
|
|
33
38
|
}
|
|
34
|
-
catch {
|
|
39
|
+
catch (err) {
|
|
40
|
+
if (err instanceof SyntaxError) {
|
|
41
|
+
console.error(`[agent-sh] Warning: invalid JSON in ${SETTINGS_PATH}: ${err.message}`);
|
|
42
|
+
}
|
|
35
43
|
cached = {};
|
|
36
44
|
}
|
|
37
45
|
}
|
|
@@ -59,3 +67,38 @@ export function getExtensionSettings(namespace, defaults) {
|
|
|
59
67
|
export function reloadSettings() {
|
|
60
68
|
cached = null;
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Expand $ENV_VAR references in a string.
|
|
72
|
+
* Supports $VAR and ${VAR} syntax.
|
|
73
|
+
*/
|
|
74
|
+
export function expandEnvVars(value) {
|
|
75
|
+
return value.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, braced, plain) => {
|
|
76
|
+
const name = braced || plain;
|
|
77
|
+
return process.env[name] ?? "";
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a provider config by name from settings.
|
|
82
|
+
* Returns null if provider not found.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveProvider(name) {
|
|
85
|
+
const settings = getSettings();
|
|
86
|
+
const provider = settings.providers?.[name];
|
|
87
|
+
if (!provider)
|
|
88
|
+
return null;
|
|
89
|
+
const models = provider.models ?? (provider.defaultModel ? [provider.defaultModel] : []);
|
|
90
|
+
const defaultModel = provider.defaultModel ?? models[0];
|
|
91
|
+
return {
|
|
92
|
+
id: name,
|
|
93
|
+
apiKey: provider.apiKey ? expandEnvVars(provider.apiKey) : undefined,
|
|
94
|
+
baseURL: provider.baseURL,
|
|
95
|
+
defaultModel,
|
|
96
|
+
models: models.length ? models : (defaultModel ? [defaultModel] : []),
|
|
97
|
+
contextWindow: provider.contextWindow,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Get all configured provider names. */
|
|
101
|
+
export function getProviderNames() {
|
|
102
|
+
const settings = getSettings();
|
|
103
|
+
return Object.keys(settings.providers ?? {});
|
|
104
|
+
}
|
package/dist/shell.js
CHANGED
|
@@ -4,6 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import * as pty from "node-pty";
|
|
5
5
|
import { InputHandler } from "./input-handler.js";
|
|
6
6
|
import { OutputParser } from "./output-parser.js";
|
|
7
|
+
import { getSettings } from "./settings.js";
|
|
7
8
|
export class Shell {
|
|
8
9
|
ptyProcess;
|
|
9
10
|
bus;
|
|
@@ -40,13 +41,16 @@ export class Shell {
|
|
|
40
41
|
let shellArgs;
|
|
41
42
|
const osc7Cmd = 'printf "\\e]7;file://%s%s\\a" "$(hostname)" "$PWD"';
|
|
42
43
|
const promptMarker = 'printf "\\e]9999;PROMPT\\a"';
|
|
44
|
+
const titleCmd = 'printf "\\e]0;⚡ agent-sh: %s\\a" "${PWD/#$HOME/~}"';
|
|
43
45
|
this.isZsh = isZsh;
|
|
46
|
+
const settings = getSettings();
|
|
47
|
+
const showIndicator = settings.promptIndicator !== false;
|
|
44
48
|
if (isZsh) {
|
|
45
49
|
// For zsh: use ZDOTDIR to source user's real config, then append
|
|
46
50
|
// our hooks via precmd_functions (additive — doesn't clobber p10k/omz).
|
|
47
51
|
this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-sh-"));
|
|
48
52
|
const userZdotdir = env.ZDOTDIR || env.HOME || os.homedir();
|
|
49
|
-
|
|
53
|
+
const zshrcLines = [
|
|
50
54
|
`ZDOTDIR="${userZdotdir}"`,
|
|
51
55
|
`[ -f "${userZdotdir}/.zshrc" ] && source "${userZdotdir}/.zshrc"`,
|
|
52
56
|
"",
|
|
@@ -54,32 +58,19 @@ export class Shell {
|
|
|
54
58
|
"__agent_sh_precmd() {",
|
|
55
59
|
` ${osc7Cmd}`,
|
|
56
60
|
` ${promptMarker}`,
|
|
61
|
+
...(showIndicator ? [` ${titleCmd}`] : []),
|
|
57
62
|
"}",
|
|
58
63
|
"precmd_functions+=(__agent_sh_precmd)",
|
|
59
64
|
"",
|
|
60
|
-
"#
|
|
61
|
-
"#
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
" __agent_sh_line_init() {",
|
|
65
|
-
" zle __agent_sh_orig_line_init",
|
|
66
|
-
' printf "\\e]9998;READY\\a"',
|
|
67
|
-
" }",
|
|
68
|
-
"else",
|
|
69
|
-
" __agent_sh_line_init() {",
|
|
70
|
-
' printf "\\e]9998;READY\\a"',
|
|
71
|
-
" }",
|
|
72
|
-
"fi",
|
|
73
|
-
"zle -N zle-line-init __agent_sh_line_init",
|
|
74
|
-
"",
|
|
75
|
-
"# Hidden widget to trigger prompt redraw from Node.js side",
|
|
76
|
-
"# Bound to an unused escape sequence that no real key produces",
|
|
77
|
-
"__agent_sh_redraw() {",
|
|
78
|
-
" zle reset-prompt",
|
|
65
|
+
"# Preexec hook: emit actual command text so agent-sh can track",
|
|
66
|
+
"# history-recalled and tab-completed commands accurately",
|
|
67
|
+
"__agent_sh_preexec() {",
|
|
68
|
+
' printf "\\e]9997;%s\\a" "$1"',
|
|
79
69
|
"}",
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
70
|
+
"preexec_functions+=(__agent_sh_preexec)",
|
|
71
|
+
];
|
|
72
|
+
zshrcLines.push("", "# End-of-prompt marker via zle-line-init (fires after prompt is rendered)", "# Chain onto existing widget (p10k uses zle-line-init) rather than clobbering", 'if (( ${+widgets[zle-line-init]} )); then', " zle -A zle-line-init __agent_sh_orig_line_init", " __agent_sh_line_init() {", " zle __agent_sh_orig_line_init", ' printf "\\e]9998;READY\\a"', " }", "else", " __agent_sh_line_init() {", ' printf "\\e]9998;READY\\a"', " }", "fi", "zle -N zle-line-init __agent_sh_line_init", "", "# Hidden widget to trigger prompt redraw from Node.js side", "# Bound to an unused escape sequence that no real key produces", "__agent_sh_redraw() {", " zle reset-prompt", "}", "zle -N __agent_sh_redraw", "bindkey '\\e[9999~' __agent_sh_redraw");
|
|
73
|
+
fs.writeFileSync(path.join(this.tmpDir, ".zshrc"), zshrcLines.join("\n") + "\n");
|
|
83
74
|
env.ZDOTDIR = this.tmpDir;
|
|
84
75
|
shellArgs = ["--no-globalrcs"];
|
|
85
76
|
}
|
|
@@ -88,15 +79,29 @@ export class Shell {
|
|
|
88
79
|
// real bashrc then appends our hooks. No HOME override needed.
|
|
89
80
|
this.tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agent-sh-"));
|
|
90
81
|
const userHome = env.HOME || os.homedir();
|
|
91
|
-
|
|
82
|
+
const bashrcLines = [
|
|
92
83
|
`[ -f "${userHome}/.bashrc" ] && source "${userHome}/.bashrc"`,
|
|
93
84
|
"",
|
|
94
85
|
"# agent-sh hooks (invisible OSC sequences for cwd + prompt detection)",
|
|
95
|
-
`PROMPT_COMMAND="\${PROMPT_COMMAND:+\$PROMPT_COMMAND;}${osc7Cmd}; ${promptMarker}"`,
|
|
86
|
+
`PROMPT_COMMAND="\${PROMPT_COMMAND:+\$PROMPT_COMMAND;}__agent_sh_preexec_ran=0; ${osc7Cmd}; ${promptMarker}${showIndicator ? `; ${titleCmd}` : ""}"`,
|
|
87
|
+
"",
|
|
88
|
+
"# Preexec hook via DEBUG trap: emit actual command text so agent-sh",
|
|
89
|
+
"# can track history-recalled and tab-completed commands accurately",
|
|
90
|
+
"__agent_sh_preexec_ran=0",
|
|
91
|
+
"__agent_sh_emit_preexec() {",
|
|
92
|
+
' [[ $__agent_sh_preexec_ran == 1 ]] && return',
|
|
93
|
+
' [[ -n $COMP_LINE ]] && return',
|
|
94
|
+
" __agent_sh_preexec_ran=1",
|
|
95
|
+
" local this_cmd",
|
|
96
|
+
` this_cmd=$(HISTTIMEFORMAT='' builtin history 1 | command sed 's/^ *[0-9]* *//')`,
|
|
97
|
+
` printf '\\e]9997;%s\\a' "$this_cmd"`,
|
|
98
|
+
"}",
|
|
99
|
+
"trap '__agent_sh_emit_preexec' DEBUG",
|
|
96
100
|
"",
|
|
97
101
|
"# End-of-prompt marker: append to PS1 (\\[...\\] marks it zero-width)",
|
|
98
102
|
'case "$PS1" in *9998*) ;; *) PS1="${PS1}\\[\\e]9998;READY\\a\\]";; esac',
|
|
99
|
-
]
|
|
103
|
+
];
|
|
104
|
+
fs.writeFileSync(path.join(this.tmpDir, ".bashrc"), bashrcLines.join("\n") + "\n");
|
|
100
105
|
shellArgs = ["--rcfile", path.join(this.tmpDir, ".bashrc")];
|
|
101
106
|
}
|
|
102
107
|
// Pause stdin before spawning PTY to avoid TTY contention on macOS.
|
|
@@ -259,6 +264,7 @@ export class Shell {
|
|
|
259
264
|
this.echoSkip = true;
|
|
260
265
|
this.paused = false;
|
|
261
266
|
process.stdout.write("\n");
|
|
267
|
+
this.bus.emit("shell:agent-exec-start", {});
|
|
262
268
|
const output = await new Promise((resolve, reject) => {
|
|
263
269
|
const timeout = setTimeout(() => {
|
|
264
270
|
this.bus.off("shell:command-done", handler);
|
|
@@ -276,6 +282,7 @@ export class Shell {
|
|
|
276
282
|
});
|
|
277
283
|
this.paused = true;
|
|
278
284
|
this.echoSkip = false;
|
|
285
|
+
this.bus.emit("shell:agent-exec-done", {});
|
|
279
286
|
return { ...payload, output: output.output, cwd: output.cwd, done: true };
|
|
280
287
|
});
|
|
281
288
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,18 +1,38 @@
|
|
|
1
1
|
import type { EventBus } from "./event-bus.js";
|
|
2
2
|
import type { ContextManager } from "./context-manager.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { LlmClient } from "./utils/llm-client.js";
|
|
4
4
|
import type { ColorPalette } from "./utils/palette.js";
|
|
5
5
|
import type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
6
|
+
import type { ToolDefinition } from "./agent/types.js";
|
|
6
7
|
export type { ContentBlock } from "./event-bus.js";
|
|
7
8
|
export type { BlockTransformOptions, FencedBlockTransformOptions } from "./utils/stream-transform.js";
|
|
9
|
+
/** A model entry in the cycling list, optionally tied to a provider. */
|
|
10
|
+
export interface AgentMode {
|
|
11
|
+
model: string;
|
|
12
|
+
/** Provider id — when cycling changes provider, LlmClient is reconfigured. */
|
|
13
|
+
provider?: string;
|
|
14
|
+
/** Provider-specific config for reconfiguring LlmClient on switch. */
|
|
15
|
+
providerConfig?: {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
baseURL?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Context window size in tokens (for usage display). */
|
|
20
|
+
contextWindow?: number;
|
|
21
|
+
/** Model supports reasoning/thinking tokens. */
|
|
22
|
+
reasoning?: boolean;
|
|
23
|
+
/** Provider supports the reasoning_effort parameter. */
|
|
24
|
+
supportsReasoningEffort?: boolean;
|
|
25
|
+
}
|
|
8
26
|
export interface AgentShellConfig {
|
|
9
|
-
agentCommand: string;
|
|
10
|
-
agentArgs: string[];
|
|
11
27
|
shell?: string;
|
|
12
28
|
model?: string;
|
|
13
29
|
extensions?: string[];
|
|
14
|
-
/**
|
|
15
|
-
|
|
30
|
+
/** API key for OpenAI-compatible provider. */
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
/** Base URL for OpenAI-compatible API. */
|
|
33
|
+
baseURL?: string;
|
|
34
|
+
/** Named provider to use from settings.json. */
|
|
35
|
+
provider?: string;
|
|
16
36
|
}
|
|
17
37
|
/**
|
|
18
38
|
* Context passed to user/third-party extensions.
|
|
@@ -23,7 +43,8 @@ export interface AgentShellConfig {
|
|
|
23
43
|
export interface ExtensionContext {
|
|
24
44
|
bus: EventBus;
|
|
25
45
|
contextManager: ContextManager;
|
|
26
|
-
|
|
46
|
+
/** LLM client for fast-path features (null in ACP mode). */
|
|
47
|
+
llmClient: LlmClient | null;
|
|
27
48
|
quit: () => void;
|
|
28
49
|
/** Override color palette slots for theming. */
|
|
29
50
|
setPalette: (overrides: Partial<ColorPalette>) => void;
|
|
@@ -33,6 +54,12 @@ export interface ExtensionContext {
|
|
|
33
54
|
createFencedBlockTransform: (opts: FencedBlockTransformOptions) => void;
|
|
34
55
|
/** Read extension-namespaced settings from ~/.agent-sh/settings.json. */
|
|
35
56
|
getExtensionSettings: <T extends Record<string, unknown>>(namespace: string, defaults: T) => T;
|
|
57
|
+
/** Register a slash command available in any input mode. */
|
|
58
|
+
registerCommand: (name: string, description: string, handler: (args: string) => Promise<void> | void) => void;
|
|
59
|
+
/** Register a tool for the built-in agent. No-op when using bridge backends. */
|
|
60
|
+
registerTool: (tool: ToolDefinition) => void;
|
|
61
|
+
/** Get all registered tools (for subagent tool subsets). Returns [] when using bridge backends. */
|
|
62
|
+
getTools: () => ToolDefinition[];
|
|
36
63
|
/** Register a named handler. */
|
|
37
64
|
define: (name: string, fn: (...args: any[]) => any) => void;
|
|
38
65
|
/** Wrap a named handler. Receives `next` (original) + args. */
|
|
@@ -6,8 +6,10 @@ export interface BoxFrameOptions {
|
|
|
6
6
|
style?: BorderStyle;
|
|
7
7
|
/** Border color (ANSI escape). Default DIM. */
|
|
8
8
|
borderColor?: string;
|
|
9
|
-
/** Title text shown
|
|
9
|
+
/** Title text shown on the left of the top border. */
|
|
10
10
|
title?: string;
|
|
11
|
+
/** Title text shown on the right of the top border. */
|
|
12
|
+
titleRight?: string;
|
|
11
13
|
/** Footer lines shown below a divider, inside the box. */
|
|
12
14
|
footer?: string[];
|
|
13
15
|
}
|
package/dist/utils/box-frame.js
CHANGED
|
@@ -30,11 +30,18 @@ export function renderBoxFrame(content, opts) {
|
|
|
30
30
|
// Content area width = total - 2 borders - 2 padding spaces
|
|
31
31
|
const innerW = Math.max(1, width - 4);
|
|
32
32
|
const output = [];
|
|
33
|
-
// Top border (with optional
|
|
34
|
-
if (opts.title) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
// Top border (with optional left/right titles)
|
|
34
|
+
if (opts.title || opts.titleRight) {
|
|
35
|
+
const leftPart = opts.title
|
|
36
|
+
? `${p.reset} ${opts.title} ${bc}`
|
|
37
|
+
: "";
|
|
38
|
+
const leftVis = opts.title ? visibleLen(opts.title) + 2 : 0; // +2 for spaces
|
|
39
|
+
const rightPart = opts.titleRight
|
|
40
|
+
? `${p.reset} ${opts.titleRight} ${bc}`
|
|
41
|
+
: "";
|
|
42
|
+
const rightVis = opts.titleRight ? visibleLen(opts.titleRight) + 2 : 0;
|
|
43
|
+
const dashCount = Math.max(1, width - 2 - leftVis - rightVis);
|
|
44
|
+
output.push(`${bc}${b.tl}${leftPart}${b.h.repeat(dashCount)}${rightPart}${b.tr}${p.reset}`);
|
|
38
45
|
}
|
|
39
46
|
else {
|
|
40
47
|
output.push(`${bc}${b.tl}${b.h.repeat(width - 2)}${b.tr}${p.reset}`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin, stateless wrapper around the OpenAI SDK.
|
|
3
|
+
* No agent-sh knowledge — just a configured client.
|
|
4
|
+
*
|
|
5
|
+
* Used by both AgentLoop (full tool loop) and fast-path features
|
|
6
|
+
* (command suggestions, completions).
|
|
7
|
+
*/
|
|
8
|
+
import OpenAI from "openai";
|
|
9
|
+
import type { ChatCompletionMessageParam, ChatCompletionTool } from "openai/resources/chat/completions.js";
|
|
10
|
+
export type { ChatCompletionMessageParam, ChatCompletionTool };
|
|
11
|
+
export interface LlmClientConfig {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
baseURL?: string;
|
|
14
|
+
model: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class LlmClient {
|
|
17
|
+
private config;
|
|
18
|
+
private client;
|
|
19
|
+
model: string;
|
|
20
|
+
constructor(config: LlmClientConfig);
|
|
21
|
+
/** Swap the underlying client config at runtime (e.g. provider switch). */
|
|
22
|
+
reconfigure(newConfig: LlmClientConfig): void;
|
|
23
|
+
/**
|
|
24
|
+
* Create a streaming chat completion.
|
|
25
|
+
* Returns an async iterable of chunks.
|
|
26
|
+
*/
|
|
27
|
+
stream(opts: {
|
|
28
|
+
messages: ChatCompletionMessageParam[];
|
|
29
|
+
tools?: ChatCompletionTool[];
|
|
30
|
+
model?: string;
|
|
31
|
+
max_tokens?: number;
|
|
32
|
+
/** Reasoning effort level (e.g. "low", "medium", "high"). Provider-dependent. */
|
|
33
|
+
reasoning_effort?: string;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
}): import("openai").APIPromise<import("openai/core/streaming.mjs").Stream<OpenAI.Chat.Completions.ChatCompletionChunk>>;
|
|
36
|
+
/**
|
|
37
|
+
* Single-shot completion (no streaming) — for fast-path features.
|
|
38
|
+
* Returns the text content of the first choice.
|
|
39
|
+
*/
|
|
40
|
+
complete(opts: {
|
|
41
|
+
messages: ChatCompletionMessageParam[];
|
|
42
|
+
model?: string;
|
|
43
|
+
max_tokens?: number;
|
|
44
|
+
}): Promise<string>;
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin, stateless wrapper around the OpenAI SDK.
|
|
3
|
+
* No agent-sh knowledge — just a configured client.
|
|
4
|
+
*
|
|
5
|
+
* Used by both AgentLoop (full tool loop) and fast-path features
|
|
6
|
+
* (command suggestions, completions).
|
|
7
|
+
*/
|
|
8
|
+
import OpenAI from "openai";
|
|
9
|
+
export class LlmClient {
|
|
10
|
+
config;
|
|
11
|
+
client;
|
|
12
|
+
model;
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.client = new OpenAI({
|
|
16
|
+
apiKey: config.apiKey,
|
|
17
|
+
baseURL: config.baseURL,
|
|
18
|
+
});
|
|
19
|
+
this.model = config.model;
|
|
20
|
+
}
|
|
21
|
+
/** Swap the underlying client config at runtime (e.g. provider switch). */
|
|
22
|
+
reconfigure(newConfig) {
|
|
23
|
+
this.config = newConfig;
|
|
24
|
+
this.client = new OpenAI({
|
|
25
|
+
apiKey: newConfig.apiKey,
|
|
26
|
+
baseURL: newConfig.baseURL,
|
|
27
|
+
});
|
|
28
|
+
this.model = newConfig.model;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create a streaming chat completion.
|
|
32
|
+
* Returns an async iterable of chunks.
|
|
33
|
+
*/
|
|
34
|
+
stream(opts) {
|
|
35
|
+
const body = {
|
|
36
|
+
model: opts.model ?? this.model,
|
|
37
|
+
messages: opts.messages,
|
|
38
|
+
tools: opts.tools?.length ? opts.tools : undefined,
|
|
39
|
+
max_tokens: opts.max_tokens ?? 8192,
|
|
40
|
+
stream: true,
|
|
41
|
+
stream_options: { include_usage: true },
|
|
42
|
+
...(opts.reasoning_effort
|
|
43
|
+
? { reasoning_effort: opts.reasoning_effort }
|
|
44
|
+
: {}),
|
|
45
|
+
};
|
|
46
|
+
return this.client.chat.completions.create(body, { signal: opts.signal });
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Single-shot completion (no streaming) — for fast-path features.
|
|
50
|
+
* Returns the text content of the first choice.
|
|
51
|
+
*/
|
|
52
|
+
async complete(opts) {
|
|
53
|
+
const response = await this.client.chat.completions.create({
|
|
54
|
+
model: opts.model ?? this.model,
|
|
55
|
+
messages: opts.messages,
|
|
56
|
+
max_tokens: opts.max_tokens ?? 1024,
|
|
57
|
+
});
|
|
58
|
+
return response.choices[0]?.message?.content ?? "";
|
|
59
|
+
}
|
|
60
|
+
}
|
package/dist/utils/markdown.js
CHANGED
|
@@ -224,9 +224,9 @@ export class MarkdownRenderer {
|
|
|
224
224
|
const h4 = line.match(/^#{4,} (.+)/);
|
|
225
225
|
if (h4)
|
|
226
226
|
return `${p.bold}${h4[1]}${p.reset}`;
|
|
227
|
-
// Horizontal rule
|
|
227
|
+
// Horizontal rule — subtle short separator, not full-width
|
|
228
228
|
if (/^(-{3,}|_{3,}|\*{3,})\s*$/.test(line)) {
|
|
229
|
-
return
|
|
229
|
+
return "";
|
|
230
230
|
}
|
|
231
231
|
// Blockquote
|
|
232
232
|
const bq = line.match(/^>\s?(.*)/);
|
|
@@ -19,37 +19,23 @@
|
|
|
19
19
|
export function createBlockTransform(bus, opts) {
|
|
20
20
|
let buffer = "";
|
|
21
21
|
bus.onPipe("agent:response-chunk", (e) => {
|
|
22
|
-
// Process text from e.text and from text blocks in e.blocks
|
|
23
22
|
const outBlocks = [];
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
else {
|
|
34
|
-
// Pass through non-text blocks unchanged
|
|
35
|
-
outBlocks.push(block);
|
|
36
|
-
}
|
|
23
|
+
for (const block of e.blocks) {
|
|
24
|
+
if (block.type === "text") {
|
|
25
|
+
buffer += block.text;
|
|
26
|
+
const { blocks: parsed, pending } = processBuffer(buffer, opts);
|
|
27
|
+
buffer = pending;
|
|
28
|
+
outBlocks.push(...parsed);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
outBlocks.push(block);
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
|
-
|
|
40
|
-
if (e.text) {
|
|
41
|
-
buffer += e.text;
|
|
42
|
-
const { blocks: parsed, pending } = processBuffer(buffer, opts);
|
|
43
|
-
buffer = pending;
|
|
44
|
-
outBlocks.push(...parsed);
|
|
45
|
-
}
|
|
46
|
-
return { ...e, text: "", blocks: outBlocks };
|
|
34
|
+
return { blocks: outBlocks };
|
|
47
35
|
});
|
|
48
36
|
bus.onPipe("agent:response-done", (e) => {
|
|
49
37
|
if (buffer) {
|
|
50
|
-
// Unclosed pattern — flush as text
|
|
51
38
|
bus.emitTransform("agent:response-chunk", {
|
|
52
|
-
text: buffer,
|
|
53
39
|
blocks: [{ type: "text", text: buffer }],
|
|
54
40
|
});
|
|
55
41
|
buffer = "";
|
|
@@ -66,35 +52,23 @@ export function createFencedBlockTransform(bus, opts) {
|
|
|
66
52
|
bus.onPipe("agent:response-chunk", (e) => {
|
|
67
53
|
if (flushing)
|
|
68
54
|
return e; // pass through during flush to avoid re-buffering
|
|
69
|
-
//
|
|
55
|
+
// Separate text blocks (to buffer) from non-text blocks (pass through)
|
|
70
56
|
let incoming = "";
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
else {
|
|
79
|
-
passthrough.push(block);
|
|
80
|
-
}
|
|
57
|
+
const passthrough = [];
|
|
58
|
+
for (const block of e.blocks) {
|
|
59
|
+
if (block.type === "text") {
|
|
60
|
+
incoming += block.text;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
passthrough.push(block);
|
|
81
64
|
}
|
|
82
|
-
const { blocks, pending } = processFencedBuffer(buffer + incoming, opts, inFence, fenceMatch, fenceLines);
|
|
83
|
-
buffer = pending.text;
|
|
84
|
-
inFence = pending.inFence;
|
|
85
|
-
fenceMatch = pending.fenceMatch;
|
|
86
|
-
fenceLines = pending.fenceLines;
|
|
87
|
-
return { ...e, text: "", blocks: [...passthrough, ...blocks] };
|
|
88
65
|
}
|
|
89
|
-
|
|
90
|
-
incoming = buffer + e.text;
|
|
91
|
-
const { blocks, pending } = processFencedBuffer(incoming, opts, inFence, fenceMatch, fenceLines);
|
|
66
|
+
const { blocks, pending } = processFencedBuffer(buffer + incoming, opts, inFence, fenceMatch, fenceLines);
|
|
92
67
|
buffer = pending.text;
|
|
93
68
|
inFence = pending.inFence;
|
|
94
69
|
fenceMatch = pending.fenceMatch;
|
|
95
70
|
fenceLines = pending.fenceLines;
|
|
96
|
-
|
|
97
|
-
return { ...e, text: "", blocks: [...existing, ...blocks] };
|
|
71
|
+
return { blocks: [...passthrough, ...blocks] };
|
|
98
72
|
});
|
|
99
73
|
function flushBuffer() {
|
|
100
74
|
if (!buffer && !inFence)
|
|
@@ -110,7 +84,6 @@ export function createFencedBlockTransform(bus, opts) {
|
|
|
110
84
|
if (remaining) {
|
|
111
85
|
flushing = true;
|
|
112
86
|
bus.emitTransform("agent:response-chunk", {
|
|
113
|
-
text: "",
|
|
114
87
|
blocks: [{ type: "text", text: remaining }],
|
|
115
88
|
});
|
|
116
89
|
flushing = false;
|
|
@@ -73,6 +73,15 @@ export function renderToolCall(tool, width) {
|
|
|
73
73
|
if (typeof raw.command === "string") {
|
|
74
74
|
detail = `$ ${raw.command}`;
|
|
75
75
|
}
|
|
76
|
+
else if (typeof raw.pattern === "string") {
|
|
77
|
+
// grep/glob — show the search pattern
|
|
78
|
+
const target = typeof raw.path === "string" ? ` ${shortenPath(raw.path, cwd)}` : "";
|
|
79
|
+
detail = `${raw.pattern}${target}`;
|
|
80
|
+
}
|
|
81
|
+
else if (typeof raw.path === "string") {
|
|
82
|
+
// read_file, write_file, etc.
|
|
83
|
+
detail = shortenPath(raw.path, cwd);
|
|
84
|
+
}
|
|
76
85
|
else if (typeof raw.operation === "string") {
|
|
77
86
|
detail = raw.operation;
|
|
78
87
|
if (raw.ids && Array.isArray(raw.ids)) {
|
|
@@ -83,20 +92,21 @@ export function renderToolCall(tool, width) {
|
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
else {
|
|
86
|
-
detail = formatRawInput(tool.rawInput, width -
|
|
95
|
+
detail = formatRawInput(tool.rawInput, width - 4);
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
}
|
|
91
|
-
// Render as single line:
|
|
92
|
-
|
|
100
|
+
// Render as single line: icon + detail (icon implies the tool type)
|
|
101
|
+
// Falls back to icon + title when no detail is available
|
|
102
|
+
const maxDetailW = Math.max(1, width - 4);
|
|
93
103
|
if (detail) {
|
|
94
104
|
if (detail.length > maxDetailW)
|
|
95
105
|
detail = detail.slice(0, maxDetailW - 1) + "…";
|
|
96
|
-
lines.push(`${p.warning}${
|
|
106
|
+
lines.push(`${p.warning}${icon}${p.reset} ${p.dim}${detail}${p.reset}`);
|
|
97
107
|
}
|
|
98
108
|
else {
|
|
99
|
-
lines.push(`${p.warning}${
|
|
109
|
+
lines.push(`${p.warning}${icon} ${tool.title}${p.reset}`);
|
|
100
110
|
}
|
|
101
111
|
// Show additional file locations on separate lines (if more than one)
|
|
102
112
|
if (mode === "full" && tool.locations && tool.locations.length > 1) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# claude-code-bridge
|
|
2
|
+
|
|
3
|
+
Runs Claude Code as an agent-sh backend using the official [@anthropic-ai/claude-agent-sdk](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Copy or symlink into your extensions directory
|
|
9
|
+
cp -r examples/extensions/claude-code-bridge ~/.agent-sh/extensions/claude-code-bridge
|
|
10
|
+
|
|
11
|
+
# Install dependencies
|
|
12
|
+
cd ~/.agent-sh/extensions/claude-code-bridge
|
|
13
|
+
npm install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Configure
|
|
17
|
+
|
|
18
|
+
Set as default backend in `~/.agent-sh/settings.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"defaultBackend": "claude-code"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or switch at runtime:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
? /backend claude-code
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- `ANTHROPIC_API_KEY` must be set in your environment
|
|
35
|
+
- Claude Code manages its own model selection — no model configuration needed in agent-sh
|