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/settings.d.ts
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
export declare const CONFIG_DIR: string;
|
|
2
|
+
/** Provider profile — a named LLM configuration. */
|
|
3
|
+
export interface ProviderConfig {
|
|
4
|
+
/** API key (supports $ENV_VAR syntax for runtime expansion). */
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
/** Base URL for OpenAI-compatible API. */
|
|
7
|
+
baseURL?: string;
|
|
8
|
+
/** Default model to use. Falls back to first entry in models list. */
|
|
9
|
+
defaultModel?: string;
|
|
10
|
+
/** Models available for cycling. */
|
|
11
|
+
models?: string[];
|
|
12
|
+
/** Context window size in tokens (e.g. 128000). Used for usage display. */
|
|
13
|
+
contextWindow?: number;
|
|
14
|
+
}
|
|
2
15
|
export interface Settings {
|
|
3
16
|
/** Extensions to load (npm packages or file paths). */
|
|
4
17
|
extensions?: string[];
|
|
5
18
|
/** Max agent query history entries to keep. */
|
|
6
19
|
historySize?: number;
|
|
20
|
+
/** Named provider configurations. */
|
|
21
|
+
providers?: Record<string, ProviderConfig>;
|
|
22
|
+
/** Which provider to use by default. */
|
|
23
|
+
defaultProvider?: string;
|
|
24
|
+
/** Preferred agent backend (extension name, e.g. "pi", "claude-code"). */
|
|
25
|
+
defaultBackend?: string;
|
|
7
26
|
/** Recent exchanges included in agent context window. */
|
|
8
27
|
contextWindowSize?: number;
|
|
9
28
|
/** Context budget in bytes (~4 chars per token). */
|
|
@@ -22,8 +41,12 @@ export interface Settings {
|
|
|
22
41
|
readOutputMaxLines?: number;
|
|
23
42
|
/** Max diff lines shown before "ctrl+o to expand". */
|
|
24
43
|
diffMaxLines?: number;
|
|
25
|
-
/**
|
|
26
|
-
|
|
44
|
+
/** Additional directories to scan for skills (supports ~ expansion). */
|
|
45
|
+
skillPaths?: string[];
|
|
46
|
+
/** Show a startup banner when agent-sh launches. */
|
|
47
|
+
startupBanner?: boolean;
|
|
48
|
+
/** Show a subtle agent-sh indicator in the shell prompt. */
|
|
49
|
+
promptIndicator?: boolean;
|
|
27
50
|
}
|
|
28
51
|
declare const DEFAULTS: Required<Settings>;
|
|
29
52
|
/** Load settings from disk (cached after first call). */
|
|
@@ -41,4 +64,32 @@ export declare function getSettings(): Settings & typeof DEFAULTS;
|
|
|
41
64
|
export declare function getExtensionSettings<T extends Record<string, unknown>>(namespace: string, defaults: T): T;
|
|
42
65
|
/** Reset cached settings (for testing or after external edit). */
|
|
43
66
|
export declare function reloadSettings(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Expand $ENV_VAR references in a string.
|
|
69
|
+
* Supports $VAR and ${VAR} syntax.
|
|
70
|
+
*/
|
|
71
|
+
export declare function expandEnvVars(value: string): string;
|
|
72
|
+
/** Resolved provider ready for use (env vars expanded, defaults applied). */
|
|
73
|
+
export interface ResolvedProvider {
|
|
74
|
+
id: string;
|
|
75
|
+
apiKey?: string;
|
|
76
|
+
baseURL?: string;
|
|
77
|
+
defaultModel?: string;
|
|
78
|
+
models: string[];
|
|
79
|
+
contextWindow?: number;
|
|
80
|
+
/** Provider supports the reasoning_effort parameter. Default: true. */
|
|
81
|
+
supportsReasoningEffort?: boolean;
|
|
82
|
+
/** Per-model capabilities, keyed by model id. */
|
|
83
|
+
modelCapabilities?: Map<string, {
|
|
84
|
+
reasoning?: boolean;
|
|
85
|
+
contextWindow?: number;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve a provider config by name from settings.
|
|
90
|
+
* Returns null if provider not found.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveProvider(name: string): ResolvedProvider | null;
|
|
93
|
+
/** Get all configured provider names. */
|
|
94
|
+
export declare function getProviderNames(): string[];
|
|
44
95
|
export {};
|
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,
|
|
@@ -19,9 +22,11 @@ const DEFAULTS = {
|
|
|
19
22
|
shellTailLines: 5,
|
|
20
23
|
recallExpandMaxLines: 100,
|
|
21
24
|
maxCommandOutputLines: 3,
|
|
22
|
-
readOutputMaxLines:
|
|
25
|
+
readOutputMaxLines: 10,
|
|
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);
|
|
@@ -268,7 +274,7 @@ export class Shell {
|
|
|
268
274
|
const handler = (e) => {
|
|
269
275
|
clearTimeout(timeout);
|
|
270
276
|
this.bus.off("shell:command-done", handler);
|
|
271
|
-
resolve({ output: e.output, cwd: e.cwd });
|
|
277
|
+
resolve({ output: e.output, cwd: e.cwd, exitCode: e.exitCode });
|
|
272
278
|
};
|
|
273
279
|
this.bus.on("shell:command-done", handler);
|
|
274
280
|
this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
|
|
@@ -276,7 +282,8 @@ export class Shell {
|
|
|
276
282
|
});
|
|
277
283
|
this.paused = true;
|
|
278
284
|
this.echoSkip = false;
|
|
279
|
-
|
|
285
|
+
this.bus.emit("shell:agent-exec-done", {});
|
|
286
|
+
return { ...payload, output: output.output, cwd: output.cwd, exitCode: output.exitCode, done: true };
|
|
280
287
|
});
|
|
281
288
|
}
|
|
282
289
|
// ── Public API (used by index.ts) ──
|
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}`);
|
package/dist/utils/diff.js
CHANGED
|
@@ -39,6 +39,16 @@ export function computeDiff(oldText, newText) {
|
|
|
39
39
|
// Build LCS table and backtrack to produce diff lines
|
|
40
40
|
const a = oldText.split("\n");
|
|
41
41
|
const b = newText.split("\n");
|
|
42
|
+
// Bail out if LCS table would be too large (avoids OOM / hang)
|
|
43
|
+
if (a.length * b.length > 10_000_000) {
|
|
44
|
+
return {
|
|
45
|
+
hunks: [],
|
|
46
|
+
added: b.length,
|
|
47
|
+
removed: a.length,
|
|
48
|
+
isIdentical: false,
|
|
49
|
+
isNewFile: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
42
52
|
const dp = buildLcs(a, b);
|
|
43
53
|
const raw = backtrack(dp, a, b);
|
|
44
54
|
let added = 0;
|
|
@@ -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.d.ts
CHANGED
package/dist/utils/markdown.js
CHANGED
|
@@ -83,6 +83,7 @@ export class MarkdownRenderer {
|
|
|
83
83
|
buffer = "";
|
|
84
84
|
contentWidth;
|
|
85
85
|
firstLine = true;
|
|
86
|
+
lastLineBlank = false;
|
|
86
87
|
pendingLines = [];
|
|
87
88
|
width;
|
|
88
89
|
tableRows = [];
|
|
@@ -192,6 +193,9 @@ export class MarkdownRenderer {
|
|
|
192
193
|
}
|
|
193
194
|
// Render rows
|
|
194
195
|
const hasHeader = sepIdx.includes(1) && dataRows.length > 1;
|
|
196
|
+
// Top border
|
|
197
|
+
const topBorder = colWidths.map((w) => "─".repeat(w)).join(`─┬─`);
|
|
198
|
+
this.writeLine(`${p.dim}┌─${topBorder}─┐${p.reset}`);
|
|
195
199
|
for (let i = 0; i < dataRows.length; i++) {
|
|
196
200
|
const row = dataRows[i];
|
|
197
201
|
const isHeader = hasHeader && i === 0;
|
|
@@ -207,6 +211,9 @@ export class MarkdownRenderer {
|
|
|
207
211
|
this.writeLine(`${p.dim}├─${sep}─┤${p.reset}`);
|
|
208
212
|
}
|
|
209
213
|
}
|
|
214
|
+
// Bottom border
|
|
215
|
+
const bottomBorder = colWidths.map((w) => "─".repeat(w)).join(`─┴─`);
|
|
216
|
+
this.writeLine(`${p.dim}└─${bottomBorder}─┘${p.reset}`);
|
|
210
217
|
}
|
|
211
218
|
renderLine(line) {
|
|
212
219
|
if (line.trim() === "")
|
|
@@ -224,14 +231,24 @@ export class MarkdownRenderer {
|
|
|
224
231
|
const h4 = line.match(/^#{4,} (.+)/);
|
|
225
232
|
if (h4)
|
|
226
233
|
return `${p.bold}${h4[1]}${p.reset}`;
|
|
227
|
-
// Horizontal rule
|
|
234
|
+
// Horizontal rule — subtle short separator, not full-width
|
|
228
235
|
if (/^(-{3,}|_{3,}|\*{3,})\s*$/.test(line)) {
|
|
229
|
-
return
|
|
236
|
+
return "";
|
|
230
237
|
}
|
|
231
238
|
// Blockquote
|
|
232
239
|
const bq = line.match(/^>\s?(.*)/);
|
|
233
240
|
if (bq)
|
|
234
241
|
return `${p.muted}│${p.reset} ${p.dim}${p.italic}${this.renderInline(bq[1] || "")}${p.reset}`;
|
|
242
|
+
// Task list (checkbox items) — must come before generic unordered list
|
|
243
|
+
const task = line.match(/^(\s*)[*\-+]\s+\[([ xX])\]\s+(.*)/);
|
|
244
|
+
if (task) {
|
|
245
|
+
const indent = task[1] || "";
|
|
246
|
+
const checked = task[2] !== " ";
|
|
247
|
+
const box = checked
|
|
248
|
+
? `${p.success}☑${p.reset}`
|
|
249
|
+
: `${p.dim}☐${p.reset}`;
|
|
250
|
+
return `${indent} ${box} ${this.renderInline(task[3] || "")}`;
|
|
251
|
+
}
|
|
235
252
|
// Unordered list
|
|
236
253
|
const ul = line.match(/^(\s*)[*\-+]\s+(.*)/);
|
|
237
254
|
if (ul) {
|
|
@@ -268,9 +285,14 @@ export class MarkdownRenderer {
|
|
|
268
285
|
* The line is accumulated internally — call drainLines() to extract.
|
|
269
286
|
*/
|
|
270
287
|
writeLine(text) {
|
|
271
|
-
|
|
288
|
+
const isBlank = visibleLen(text) === 0;
|
|
289
|
+
if (this.firstLine && isBlank)
|
|
290
|
+
return;
|
|
291
|
+
// Collapse consecutive blank lines to a single one
|
|
292
|
+
if (isBlank && this.lastLineBlank)
|
|
272
293
|
return;
|
|
273
294
|
this.firstLine = false;
|
|
295
|
+
this.lastLineBlank = isBlank;
|
|
274
296
|
this.pendingLines.push(` ${text}`);
|
|
275
297
|
}
|
|
276
298
|
}
|