agent-sh 0.12.26 → 0.13.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 +13 -2
- package/dist/agent/agent-loop.d.ts +3 -5
- package/dist/agent/agent-loop.js +44 -100
- package/dist/agent/conversation-state.d.ts +9 -0
- package/dist/agent/conversation-state.js +38 -1
- package/dist/agent/history-file.d.ts +6 -0
- package/dist/agent/history-file.js +1 -1
- package/dist/agent/host-types.d.ts +125 -0
- package/dist/agent/index.d.ts +12 -4
- package/dist/agent/index.js +357 -6
- package/dist/agent/nuclear-form.d.ts +7 -0
- package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
- package/dist/{extensions → agent}/providers/deepseek.js +5 -4
- package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.js +3 -2
- package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openrouter.js +4 -3
- package/dist/agent/skills.js +51 -7
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/system-prompt.js +14 -17
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-protocol.js +5 -3
- package/dist/agent/tool-registry.d.ts +9 -4
- package/dist/agent/tool-registry.js +27 -4
- package/dist/agent/tools/bash.d.ts +1 -1
- package/dist/agent/tools/bash.js +3 -2
- package/dist/agent/tools/edit-file.js +0 -1
- package/dist/agent/tools/glob.js +1 -1
- package/dist/agent/tools/grep.js +1 -1
- package/dist/agent/tools/pwsh.d.ts +1 -1
- package/dist/agent/tools/pwsh.js +1 -2
- package/dist/agent/tools/read-file.js +7 -4
- package/dist/agent/tools/write-file.js +0 -1
- package/dist/agent/types.d.ts +17 -2
- package/dist/cli/auth/cli.d.ts +1 -0
- package/dist/cli/auth/cli.js +216 -0
- package/dist/cli/auth/keys.d.ts +31 -0
- package/dist/cli/auth/keys.js +102 -0
- package/dist/{index.js → cli/index.js} +29 -32
- package/dist/{init.js → cli/init.js} +1 -1
- package/dist/{install.js → cli/install.js} +114 -5
- package/dist/cli/subcommands.d.ts +1 -0
- package/dist/cli/subcommands.js +17 -0
- package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
- package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
- package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
- package/dist/{core.d.ts → core/index.d.ts} +18 -15
- package/dist/{core.js → core/index.js} +18 -92
- package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
- package/dist/{settings.js → core/settings.js} +1 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +1 -0
- package/dist/extensions/file-autocomplete.d.ts +1 -1
- package/dist/extensions/index.d.ts +7 -14
- package/dist/extensions/index.js +2 -19
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +7 -2
- package/dist/shell/host-types.d.ts +114 -0
- package/dist/shell/host-types.js +1 -0
- package/dist/shell/index.d.ts +8 -7
- package/dist/shell/index.js +58 -9
- package/dist/shell/input-handler.d.ts +7 -1
- package/dist/shell/input-handler.js +5 -2
- package/dist/shell/output-parser.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.js +18 -12
- package/dist/shell/shell.d.ts +6 -4
- package/dist/shell/shell.js +33 -109
- package/dist/shell/strategies/bash.d.ts +2 -0
- package/dist/shell/strategies/bash.js +68 -0
- package/dist/shell/strategies/fish.d.ts +2 -0
- package/dist/shell/strategies/fish.js +65 -0
- package/dist/shell/strategies/index.d.ts +13 -0
- package/dist/shell/strategies/index.js +17 -0
- package/dist/shell/strategies/types.d.ts +50 -0
- package/dist/shell/strategies/types.js +9 -0
- package/dist/shell/strategies/zsh.d.ts +2 -0
- package/dist/shell/strategies/zsh.js +72 -0
- package/dist/shell/tui-input-view.js +14 -3
- package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
- package/dist/{extensions → shell}/tui-renderer.js +27 -55
- package/dist/utils/box-frame.d.ts +4 -0
- package/dist/utils/box-frame.js +17 -6
- package/dist/utils/compositor.d.ts +1 -1
- package/dist/utils/compositor.js +2 -1
- package/dist/{executor.js → utils/executor.js} +1 -1
- package/dist/utils/floating-panel.d.ts +17 -5
- package/dist/utils/floating-panel.js +218 -70
- package/dist/utils/llm-facade.d.ts +7 -3
- package/dist/utils/stream-transform.d.ts +1 -1
- package/dist/utils/terminal-buffer.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -0
- package/dist/utils/tool-interactive.d.ts +1 -1
- package/dist/utils/tty.d.ts +7 -0
- package/dist/utils/tty.js +15 -0
- package/examples/extensions/ash-acp-bridge/README.md +4 -1
- package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
- package/examples/extensions/ashi/README.md +250 -0
- package/examples/extensions/ashi/package.json +60 -0
- package/examples/extensions/ashi/src/autocomplete.ts +91 -0
- package/examples/extensions/ashi/src/capture.ts +34 -0
- package/examples/extensions/ashi/src/cli.ts +126 -0
- package/examples/extensions/ashi/src/commands.ts +82 -0
- package/examples/extensions/ashi/src/compaction.ts +157 -0
- package/examples/extensions/ashi/src/components.ts +332 -0
- package/examples/extensions/ashi/src/default-renderers.ts +153 -0
- package/examples/extensions/ashi/src/display-config.ts +62 -0
- package/examples/extensions/ashi/src/frontend.ts +735 -0
- package/examples/extensions/ashi/src/hooks.ts +136 -0
- package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
- package/examples/extensions/ashi/src/session-commands.ts +76 -0
- package/examples/extensions/ashi/src/session-store.ts +264 -0
- package/examples/extensions/ashi/src/status-footer.ts +66 -0
- package/examples/extensions/ashi/src/theme.ts +151 -0
- package/examples/extensions/ashi/tsconfig.json +14 -0
- package/examples/extensions/emacs-buffer.ts +364 -0
- package/examples/extensions/interactive-prompts.ts +114 -69
- package/examples/extensions/latex-images.ts +3 -3
- package/examples/extensions/opencode-bridge/index.ts +1 -1
- package/examples/extensions/overlay-agent.ts +35 -10
- package/examples/extensions/peer-mesh.ts +1 -1
- package/examples/extensions/pi-bridge/index.ts +0 -1
- package/examples/extensions/questionnaire.ts +2 -1
- package/examples/extensions/rtk-proxy.ts +3 -3
- package/examples/extensions/solarized-theme.ts +3 -3
- package/examples/extensions/subagents.ts +6 -6
- package/examples/extensions/terminal-buffer.ts +174 -33
- package/examples/extensions/tmux-pane.ts +6 -4
- package/examples/extensions/tunnel-vision.ts +405 -0
- package/examples/extensions/user-shell.ts +1 -1
- package/examples/extensions/web-access.ts +8 -113
- package/package.json +26 -22
- package/dist/extensions/agent-backend.d.ts +0 -14
- package/dist/extensions/agent-backend.js +0 -307
- package/dist/types.d.ts +0 -227
- /package/dist/{types.js → agent/host-types.js} +0 -0
- /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
- /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
- /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
- /package/dist/{event-bus.js → core/event-bus.js} +0 -0
- /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
3
|
+
import { activateShell, registerShellHandlers } from "../shell/index.js";
|
|
4
|
+
import { pickStrategy, FALLBACK_STRATEGY } from "../shell/strategies/index.js";
|
|
5
|
+
import { activateAgent } from "../agent/index.js";
|
|
6
|
+
import { createCore } from "../core/index.js";
|
|
7
|
+
import { palette as p } from "../utils/palette.js";
|
|
8
|
+
import { loadBuiltinExtensions } from "../extensions/index.js";
|
|
9
|
+
import { loadExtensions } from "../core/extension-loader.js";
|
|
10
|
+
import { getSettings } from "../core/settings.js";
|
|
11
|
+
import { dispatchSubcommand } from "./subcommands.js";
|
|
12
|
+
import { suggestBridgeFor } from "./install.js";
|
|
13
|
+
import { anyProviderConfigured } from "./auth/keys.js";
|
|
14
|
+
import { PACKAGE_VERSION } from "../utils/package-version.js";
|
|
15
|
+
import { clearOpost } from "../utils/tty.js";
|
|
13
16
|
/**
|
|
14
17
|
* Capture the user's full shell environment.
|
|
15
18
|
* This picks up env vars exported in .zshrc/.bashrc that the
|
|
@@ -25,12 +28,9 @@ async function captureShellEnvAsync(shell) {
|
|
|
25
28
|
resolve(result);
|
|
26
29
|
};
|
|
27
30
|
try {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
? 'source ~/.zshrc 2>/dev/null;'
|
|
32
|
-
: '[ -f ~/.bashrc ] && source ~/.bashrc 2>/dev/null;';
|
|
33
|
-
const child = spawn(shell, ["-l", "-c", `${sourceRc} env -0`], {
|
|
31
|
+
const strategy = pickStrategy(shell) ?? FALLBACK_STRATEGY;
|
|
32
|
+
const captureCmd = strategy.envCaptureCommand();
|
|
33
|
+
const child = spawn(shell, ["-l", "-c", captureCmd], {
|
|
34
34
|
stdio: ["ignore", "pipe", "ignore"],
|
|
35
35
|
timeout: 5000,
|
|
36
36
|
});
|
|
@@ -119,6 +119,9 @@ Usage: agent-sh [options]
|
|
|
119
119
|
agent-sh install <spec> [--force] Install an extension (bundled name, file:, npm:, github:)
|
|
120
120
|
agent-sh uninstall <name> Remove an installed extension
|
|
121
121
|
agent-sh list List installed extensions
|
|
122
|
+
agent-sh auth login [provider] Store an API key for a built-in provider
|
|
123
|
+
agent-sh auth logout <provider> Remove a stored key
|
|
124
|
+
agent-sh auth list Show configured providers
|
|
122
125
|
|
|
123
126
|
Provider Profiles:
|
|
124
127
|
--provider <name> Use a provider from ~/.agent-sh/settings.json
|
|
@@ -161,24 +164,9 @@ Inside the shell:
|
|
|
161
164
|
return { shell, model, extensions, apiKey, baseURL, provider, backend };
|
|
162
165
|
}
|
|
163
166
|
async function main() {
|
|
164
|
-
// Subcommands — handled before the shell-launch path.
|
|
165
167
|
const rawArgs = process.argv.slice(2);
|
|
166
|
-
if (rawArgs
|
|
167
|
-
runInit({ force: rawArgs.includes("--force") });
|
|
168
|
+
if (await dispatchSubcommand(rawArgs))
|
|
168
169
|
return;
|
|
169
|
-
}
|
|
170
|
-
if (rawArgs[0] === "install") {
|
|
171
|
-
await runInstall(rawArgs[1] ?? "", { force: rawArgs.includes("--force") });
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (rawArgs[0] === "uninstall") {
|
|
175
|
-
await runUninstall(rawArgs[1] ?? "");
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (rawArgs[0] === "list") {
|
|
179
|
-
runList();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
170
|
if (process.env.AGENT_SH) {
|
|
183
171
|
console.error("agent-sh: already running inside an agent-sh session (nested sessions are not supported).");
|
|
184
172
|
process.exit(1);
|
|
@@ -212,6 +200,13 @@ async function main() {
|
|
|
212
200
|
catch {
|
|
213
201
|
// Ignore errors, we already have process.env as fallback
|
|
214
202
|
}
|
|
203
|
+
if (!config.apiKey && !config.provider && !anyProviderConfigured()) {
|
|
204
|
+
console.error("\nagent-sh: no LLM provider configured.\n\n" +
|
|
205
|
+
" Run `agent-sh auth login` to store an API key, or\n" +
|
|
206
|
+
" export OPENAI_API_KEY / OPENROUTER_API_KEY / DEEPSEEK_API_KEY, or\n" +
|
|
207
|
+
" run `agent-sh init` for a settings.json template.\n");
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
215
210
|
// ── Core (frontend-agnostic) ──────────────────────────────────
|
|
216
211
|
const core = createCore(config);
|
|
217
212
|
const { bus } = core;
|
|
@@ -239,6 +234,7 @@ async function main() {
|
|
|
239
234
|
const extCtx = core.extensionContext({ quit: cleanup });
|
|
240
235
|
// Before loadExtensions: extensions look up shell handlers at activation.
|
|
241
236
|
registerShellHandlers(extCtx);
|
|
237
|
+
activateAgent(extCtx);
|
|
242
238
|
// Load before spawning the shell so PS1 lands below the banner.
|
|
243
239
|
await loadBuiltinExtensions(extCtx, getSettings().disabledBuiltins);
|
|
244
240
|
const loadExtensionsTimeoutMs = 10000;
|
|
@@ -340,6 +336,7 @@ async function main() {
|
|
|
340
336
|
if (process.stdin.isTTY) {
|
|
341
337
|
try {
|
|
342
338
|
process.stdin.setRawMode(true);
|
|
339
|
+
clearOpost();
|
|
343
340
|
}
|
|
344
341
|
catch {
|
|
345
342
|
// May fail if stdin is not a TTY
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { CONFIG_DIR } from "
|
|
3
|
+
import { CONFIG_DIR } from "../core/settings.js";
|
|
4
4
|
const EXTENSIONS_DIR = path.join(CONFIG_DIR, "extensions");
|
|
5
5
|
const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
|
|
6
6
|
const EXAMPLE_PATH = path.join(CONFIG_DIR, "settings.example.json");
|
|
@@ -2,13 +2,13 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import { CONFIG_DIR, getSettings } from "
|
|
5
|
+
import { CONFIG_DIR, getSettings } from "../core/settings.js";
|
|
6
6
|
// Kept in sync with extension-loader.ts SCRIPT_EXTS.
|
|
7
7
|
const SCRIPT_EXTS = [".js", ".mjs", ".ts", ".tsx", ".mts"];
|
|
8
8
|
function hasIndexFile(dir) {
|
|
9
9
|
return SCRIPT_EXTS.some((ext) => fs.existsSync(path.join(dir, `index${ext}`)));
|
|
10
10
|
}
|
|
11
|
-
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../");
|
|
12
12
|
const BUNDLED_DIR = path.join(PACKAGE_ROOT, "examples/extensions");
|
|
13
13
|
const EXT_DIR = path.join(CONFIG_DIR, "extensions");
|
|
14
14
|
export function listBundled() {
|
|
@@ -69,11 +69,41 @@ function pickResolver(spec) {
|
|
|
69
69
|
return r;
|
|
70
70
|
return bundledResolver;
|
|
71
71
|
}
|
|
72
|
-
function
|
|
72
|
+
function readPackageJson(target) {
|
|
73
|
+
const pkgJson = path.join(target, "package.json");
|
|
74
|
+
if (!fs.existsSync(pkgJson))
|
|
75
|
+
return null;
|
|
76
|
+
return JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
|
|
77
|
+
}
|
|
78
|
+
/** Relative `file:` deps in bundled extensions (e.g. `"agent-sh": "file:../../.."`)
|
|
79
|
+
* point at the wrong location after the source is copied into ~/.agent-sh/extensions/.
|
|
80
|
+
* Resolve them against the original source dir so npm install in the target succeeds. */
|
|
81
|
+
function rewriteFileDeps(target, sourcePath) {
|
|
73
82
|
const pkgJson = path.join(target, "package.json");
|
|
74
83
|
if (!fs.existsSync(pkgJson))
|
|
75
84
|
return;
|
|
76
|
-
const
|
|
85
|
+
const raw = fs.readFileSync(pkgJson, "utf-8");
|
|
86
|
+
const pkg = JSON.parse(raw);
|
|
87
|
+
const sections = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
|
|
88
|
+
let changed = false;
|
|
89
|
+
for (const section of sections) {
|
|
90
|
+
const deps = pkg[section];
|
|
91
|
+
if (!deps || typeof deps !== "object")
|
|
92
|
+
continue;
|
|
93
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
94
|
+
if (typeof spec !== "string" || !spec.startsWith("file:"))
|
|
95
|
+
continue;
|
|
96
|
+
const rel = spec.slice("file:".length);
|
|
97
|
+
if (path.isAbsolute(rel))
|
|
98
|
+
continue;
|
|
99
|
+
deps[name] = `file:${path.resolve(sourcePath, rel)}`;
|
|
100
|
+
changed = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (changed)
|
|
104
|
+
fs.writeFileSync(pkgJson, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
105
|
+
}
|
|
106
|
+
function maybeNpmInstall(target, pkg) {
|
|
77
107
|
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) };
|
|
78
108
|
if (Object.keys(deps).length === 0)
|
|
79
109
|
return;
|
|
@@ -88,6 +118,56 @@ function maybeNpmInstall(target) {
|
|
|
88
118
|
throw new Error(`npm install failed in ${target}; run it manually.`);
|
|
89
119
|
}
|
|
90
120
|
}
|
|
121
|
+
function normalizeBin(pkg) {
|
|
122
|
+
if (!pkg.bin)
|
|
123
|
+
return {};
|
|
124
|
+
if (typeof pkg.bin === "string") {
|
|
125
|
+
const name = pkg.name?.startsWith("@") ? pkg.name.split("/")[1] : pkg.name;
|
|
126
|
+
return name ? { [name]: pkg.bin } : {};
|
|
127
|
+
}
|
|
128
|
+
return pkg.bin;
|
|
129
|
+
}
|
|
130
|
+
function maybeNpmBuild(target, pkg) {
|
|
131
|
+
if (!pkg.scripts?.build)
|
|
132
|
+
return;
|
|
133
|
+
const binPaths = Object.values(normalizeBin(pkg)).map((p) => path.join(target, p));
|
|
134
|
+
if (binPaths.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
if (binPaths.every((p) => fs.existsSync(p)))
|
|
137
|
+
return;
|
|
138
|
+
console.log(`Running npm run build in ${target}...`);
|
|
139
|
+
const result = spawnSync("npm", ["run", "build"], { cwd: target, stdio: "inherit" });
|
|
140
|
+
if (result.status !== 0) {
|
|
141
|
+
throw new Error(`npm run build failed in ${target}; run it manually.`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function linkBins(target, pkg) {
|
|
145
|
+
const bins = normalizeBin(pkg);
|
|
146
|
+
if (Object.keys(bins).length === 0)
|
|
147
|
+
return [];
|
|
148
|
+
const binDir = path.join(CONFIG_DIR, "bin");
|
|
149
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
150
|
+
const linked = [];
|
|
151
|
+
for (const [name, relPath] of Object.entries(bins)) {
|
|
152
|
+
const src = path.resolve(target, relPath);
|
|
153
|
+
if (!fs.existsSync(src)) {
|
|
154
|
+
console.error(`agent-sh: skipping bin "${name}" — ${src} not found`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
fs.chmodSync(src, 0o755);
|
|
159
|
+
}
|
|
160
|
+
catch { /* ignore */ }
|
|
161
|
+
const linkPath = path.join(binDir, name);
|
|
162
|
+
try {
|
|
163
|
+
fs.unlinkSync(linkPath);
|
|
164
|
+
}
|
|
165
|
+
catch { /* ignore */ }
|
|
166
|
+
fs.symlinkSync(src, linkPath);
|
|
167
|
+
linked.push(name);
|
|
168
|
+
}
|
|
169
|
+
return linked;
|
|
170
|
+
}
|
|
91
171
|
export async function runInstall(spec, opts = {}) {
|
|
92
172
|
if (!spec) {
|
|
93
173
|
console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force]\n\n" +
|
|
@@ -114,10 +194,17 @@ export async function runInstall(spec, opts = {}) {
|
|
|
114
194
|
}
|
|
115
195
|
fs.rmSync(target, { recursive: true, force: true });
|
|
116
196
|
}
|
|
197
|
+
let linkedBins = [];
|
|
117
198
|
if (resolved.isDirectory) {
|
|
118
199
|
fs.cpSync(resolved.sourcePath, target, { recursive: true });
|
|
119
200
|
try {
|
|
120
|
-
|
|
201
|
+
rewriteFileDeps(target, resolved.sourcePath);
|
|
202
|
+
const pkg = readPackageJson(target);
|
|
203
|
+
if (pkg) {
|
|
204
|
+
maybeNpmInstall(target, pkg);
|
|
205
|
+
maybeNpmBuild(target, pkg);
|
|
206
|
+
linkedBins = linkBins(target, pkg);
|
|
207
|
+
}
|
|
121
208
|
}
|
|
122
209
|
catch (err) {
|
|
123
210
|
console.error(`agent-sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -128,6 +215,11 @@ export async function runInstall(spec, opts = {}) {
|
|
|
128
215
|
fs.copyFileSync(resolved.sourcePath, target);
|
|
129
216
|
}
|
|
130
217
|
console.log(`Installed: ${resolved.name} -> ${target}`);
|
|
218
|
+
if (linkedBins.length > 0) {
|
|
219
|
+
const binDir = path.join(CONFIG_DIR, "bin");
|
|
220
|
+
console.log(`Linked bins: ${linkedBins.join(", ")} -> ${binDir}`);
|
|
221
|
+
console.log(`Add to PATH: export PATH="${binDir}:$PATH"`);
|
|
222
|
+
}
|
|
131
223
|
}
|
|
132
224
|
export async function runUninstall(name) {
|
|
133
225
|
if (!name) {
|
|
@@ -146,6 +238,23 @@ export async function runUninstall(name) {
|
|
|
146
238
|
console.error(`agent-sh: not installed: ${name}`);
|
|
147
239
|
process.exit(1);
|
|
148
240
|
}
|
|
241
|
+
const pkg = readPackageJson(target);
|
|
242
|
+
if (pkg) {
|
|
243
|
+
const binDir = path.join(CONFIG_DIR, "bin");
|
|
244
|
+
const targetPrefix = path.resolve(target) + path.sep;
|
|
245
|
+
for (const binName of Object.keys(normalizeBin(pkg))) {
|
|
246
|
+
const linkPath = path.join(binDir, binName);
|
|
247
|
+
try {
|
|
248
|
+
const stat = fs.lstatSync(linkPath, { throwIfNoEntry: false });
|
|
249
|
+
if (!stat?.isSymbolicLink())
|
|
250
|
+
continue;
|
|
251
|
+
const dest = path.resolve(binDir, fs.readlinkSync(linkPath));
|
|
252
|
+
if (dest.startsWith(targetPrefix))
|
|
253
|
+
fs.unlinkSync(linkPath);
|
|
254
|
+
}
|
|
255
|
+
catch { /* ignore */ }
|
|
256
|
+
}
|
|
257
|
+
}
|
|
149
258
|
fs.rmSync(target, { recursive: true, force: true });
|
|
150
259
|
console.log(`Uninstalled: ${name}`);
|
|
151
260
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function dispatchSubcommand(argv: string[]): Promise<boolean>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { runInit } from "./init.js";
|
|
2
|
+
import { runInstall, runUninstall, runList } from "./install.js";
|
|
3
|
+
import { runAuth } from "./auth/cli.js";
|
|
4
|
+
const SUBCOMMANDS = {
|
|
5
|
+
init: (args) => runInit({ force: args.includes("--force") }),
|
|
6
|
+
install: (args) => runInstall(args[0] ?? "", { force: args.includes("--force") }),
|
|
7
|
+
uninstall: (args) => runUninstall(args[0] ?? ""),
|
|
8
|
+
list: () => runList(),
|
|
9
|
+
auth: (args) => runAuth(args),
|
|
10
|
+
};
|
|
11
|
+
export async function dispatchSubcommand(argv) {
|
|
12
|
+
const handler = SUBCOMMANDS[argv[0] ?? ""];
|
|
13
|
+
if (!handler)
|
|
14
|
+
return false;
|
|
15
|
+
await handler(argv.slice(1));
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AgentMode } from "
|
|
2
|
-
import type { ToolResultDisplay } from "
|
|
1
|
+
import type { AgentMode } from "../agent/host-types.js";
|
|
2
|
+
import type { ToolResultDisplay } from "../agent/types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Typed event map — every event has a known payload shape.
|
|
5
5
|
*/
|
|
@@ -50,7 +50,7 @@ export interface ShellEvents {
|
|
|
50
50
|
"agent:append-user-message": {
|
|
51
51
|
text: string;
|
|
52
52
|
};
|
|
53
|
-
"input-mode:register": import("
|
|
53
|
+
"input-mode:register": import("../shell/host-types.js").InputModeConfig;
|
|
54
54
|
"agent:query": {
|
|
55
55
|
query: string;
|
|
56
56
|
};
|
|
@@ -136,6 +136,8 @@ export interface ShellEvents {
|
|
|
136
136
|
rawInput?: unknown;
|
|
137
137
|
/** Pre-formatted display detail from tool's formatCall(). */
|
|
138
138
|
displayDetail?: string;
|
|
139
|
+
/** highlight.js-style identifier for syntax-highlighting `rawInput.source`. */
|
|
140
|
+
sourceLanguage?: string;
|
|
139
141
|
batchIndex?: number;
|
|
140
142
|
batchTotal?: number;
|
|
141
143
|
};
|
|
@@ -162,14 +164,6 @@ export interface ShellEvents {
|
|
|
162
164
|
};
|
|
163
165
|
"tool:interactive-start": Record<string, never>;
|
|
164
166
|
"tool:interactive-end": Record<string, never>;
|
|
165
|
-
"permission:request": {
|
|
166
|
-
kind: string;
|
|
167
|
-
title: string;
|
|
168
|
-
metadata: Record<string, unknown>;
|
|
169
|
-
/** Interactive UI capability — available when the built-in agent is active. */
|
|
170
|
-
ui?: unknown;
|
|
171
|
-
decision: Record<string, unknown>;
|
|
172
|
-
};
|
|
173
167
|
"command:register": {
|
|
174
168
|
name: string;
|
|
175
169
|
description: string;
|
|
@@ -336,14 +330,14 @@ export interface ShellEvents {
|
|
|
336
330
|
reasoningParams?: (level: string, model?: string) => Record<string, unknown>;
|
|
337
331
|
};
|
|
338
332
|
"agent:register-tool": {
|
|
339
|
-
tool: import("
|
|
333
|
+
tool: import("../agent/types.js").ToolDefinition;
|
|
340
334
|
extensionName?: string;
|
|
341
335
|
};
|
|
342
336
|
"agent:unregister-tool": {
|
|
343
337
|
name: string;
|
|
344
338
|
};
|
|
345
339
|
"agent:get-tools": {
|
|
346
|
-
tools: import("
|
|
340
|
+
tools: import("../agent/types.js").ToolDefinition[];
|
|
347
341
|
};
|
|
348
342
|
"agent:register-instruction": {
|
|
349
343
|
name: string;
|
|
@@ -23,7 +23,6 @@ async function ensureTsSupport(force = false) {
|
|
|
23
23
|
if (tsRegistered && !force)
|
|
24
24
|
return;
|
|
25
25
|
try {
|
|
26
|
-
// Unregister previous loader if reloading
|
|
27
26
|
if (tsxUnregister) {
|
|
28
27
|
try {
|
|
29
28
|
await tsxUnregister();
|
|
@@ -39,70 +38,84 @@ async function ensureTsSupport(force = false) {
|
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
/**
|
|
42
|
-
* Wrap an ExtensionContext to track all registrations (bus.on,
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* Wrap an ExtensionContext to track all registrations (bus.on, advise,
|
|
42
|
+
* command:register, plus all agent/shell surface registrars). Returns
|
|
43
|
+
* the wrapped context and a dispose() that tears down everything
|
|
44
|
+
* registered through it.
|
|
45
45
|
*/
|
|
46
46
|
function createScopedContext(ctx, extensionName) {
|
|
47
47
|
const cleanups = [];
|
|
48
48
|
const bus = ctx.bus;
|
|
49
49
|
const scopedBus = Object.create(bus);
|
|
50
|
-
// Track bus.on registrations
|
|
51
50
|
scopedBus.on = ((event, fn) => {
|
|
52
51
|
bus.on(event, fn);
|
|
53
52
|
cleanups.push(() => bus.off(event, fn));
|
|
54
53
|
});
|
|
55
|
-
// Track bus.onPipe registrations
|
|
56
54
|
scopedBus.onPipe = ((event, fn) => {
|
|
57
55
|
bus.onPipe(event, fn);
|
|
58
56
|
cleanups.push(() => bus.offPipe(event, fn));
|
|
59
57
|
});
|
|
60
|
-
//
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
cleanups.push(
|
|
64
|
-
return
|
|
58
|
+
// Wrap any (name, fn) → unsubscribe registrar so its disposer runs on teardown.
|
|
59
|
+
const trackUnsub = (fn) => (a, b) => {
|
|
60
|
+
const unsub = fn(a, b);
|
|
61
|
+
cleanups.push(unsub);
|
|
62
|
+
return unsub;
|
|
65
63
|
};
|
|
66
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
// Track skill registrations — extension name captured in scope
|
|
72
|
-
const scopedRegisterSkill = (name, description, filePath) => {
|
|
73
|
-
bus.emit("agent:register-skill", { name, description, filePath, extensionName });
|
|
74
|
-
cleanups.push(() => bus.emit("agent:remove-skill", { name }));
|
|
75
|
-
};
|
|
76
|
-
// Track dynamic-context producer registrations
|
|
77
|
-
const scopedRegisterContextProducer = (name, producer) => {
|
|
78
|
-
const dispose = ctx.registerContextProducer(name, producer);
|
|
79
|
-
cleanups.push(dispose);
|
|
80
|
-
return dispose;
|
|
81
|
-
};
|
|
82
|
-
// Track tool registrations — extension name captured in scope
|
|
83
|
-
const scopedRegisterTool = (tool) => {
|
|
84
|
-
bus.emit("agent:register-tool", { tool, extensionName });
|
|
85
|
-
cleanups.push(() => bus.emit("agent:unregister-tool", { name: tool.name }));
|
|
86
|
-
};
|
|
87
|
-
// Track slash command registrations — without this, reloading an
|
|
88
|
-
// extension stacks its commands (old `/status` + new `/status`) in
|
|
89
|
-
// the slash-commands registry.
|
|
64
|
+
// ── substrate / sugar ──────────────────────────────────────
|
|
65
|
+
const scopedAdvise = trackUnsub(ctx.advise);
|
|
66
|
+
// Without this, reloading an extension stacks its commands (old + new)
|
|
67
|
+
// in the slash-commands registry.
|
|
90
68
|
const scopedRegisterCommand = (name, description, handler) => {
|
|
91
69
|
ctx.registerCommand(name, description, handler);
|
|
92
70
|
cleanups.push(() => bus.emit("command:unregister", { name }));
|
|
93
71
|
};
|
|
72
|
+
const scopedAdviseCommand = trackUnsub(ctx.adviseCommand);
|
|
73
|
+
// ── agent surface (optional — bridge backends omit it) ───
|
|
74
|
+
const agent = ctx.agent;
|
|
75
|
+
let scopedAgent;
|
|
76
|
+
if (agent) {
|
|
77
|
+
scopedAgent = {
|
|
78
|
+
...agent,
|
|
79
|
+
registerTool: (tool) => {
|
|
80
|
+
bus.emit("agent:register-tool", { tool, extensionName });
|
|
81
|
+
cleanups.push(() => bus.emit("agent:unregister-tool", { name: tool.name }));
|
|
82
|
+
},
|
|
83
|
+
adviseTool: trackUnsub(agent.adviseTool),
|
|
84
|
+
adviseToolSchema: trackUnsub(agent.adviseToolSchema),
|
|
85
|
+
registerInstruction: (name, text) => {
|
|
86
|
+
bus.emit("agent:register-instruction", { name, text, extensionName });
|
|
87
|
+
cleanups.push(() => bus.emit("agent:remove-instruction", { name }));
|
|
88
|
+
},
|
|
89
|
+
adviseInstruction: trackUnsub(agent.adviseInstruction),
|
|
90
|
+
registerSkill: (name, description, filePath) => {
|
|
91
|
+
bus.emit("agent:register-skill", { name, description, filePath, extensionName });
|
|
92
|
+
cleanups.push(() => bus.emit("agent:remove-skill", { name }));
|
|
93
|
+
},
|
|
94
|
+
adviseSkill: trackUnsub(agent.adviseSkill),
|
|
95
|
+
registerContextProducer: (name, producer, opts) => {
|
|
96
|
+
const dispose = agent.registerContextProducer(name, producer, opts);
|
|
97
|
+
cleanups.push(dispose);
|
|
98
|
+
return dispose;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ── shell surface (optional — headless backends omit it) ──
|
|
103
|
+
const shell = ctx.shell;
|
|
104
|
+
let scopedShell;
|
|
105
|
+
if (shell) {
|
|
106
|
+
scopedShell = {
|
|
107
|
+
...shell,
|
|
108
|
+
adviseInputMode: trackUnsub(shell.adviseInputMode),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
94
111
|
const scoped = {
|
|
95
112
|
...ctx,
|
|
96
113
|
bus: scopedBus,
|
|
97
114
|
advise: scopedAdvise,
|
|
98
|
-
registerInstruction: scopedRegisterInstruction,
|
|
99
|
-
removeInstruction: ctx.removeInstruction,
|
|
100
|
-
registerSkill: scopedRegisterSkill,
|
|
101
|
-
removeSkill: ctx.removeSkill,
|
|
102
|
-
registerContextProducer: scopedRegisterContextProducer,
|
|
103
|
-
registerTool: scopedRegisterTool,
|
|
104
|
-
unregisterTool: ctx.unregisterTool,
|
|
105
115
|
registerCommand: scopedRegisterCommand,
|
|
116
|
+
adviseCommand: scopedAdviseCommand,
|
|
117
|
+
agent: scopedAgent,
|
|
118
|
+
shell: scopedShell,
|
|
106
119
|
onDispose: (fn) => { cleanups.push(fn); },
|
|
107
120
|
};
|
|
108
121
|
const dispose = () => {
|
|
@@ -116,7 +129,6 @@ function createScopedContext(ctx, extensionName) {
|
|
|
116
129
|
};
|
|
117
130
|
return { scoped, dispose };
|
|
118
131
|
}
|
|
119
|
-
// Track disposers for user extensions so reload can tear them down
|
|
120
132
|
const extensionDisposers = new Map();
|
|
121
133
|
/**
|
|
122
134
|
* Load extensions from three sources (merged, deduplicated):
|
|
@@ -134,19 +146,15 @@ const extensionDisposers = new Map();
|
|
|
134
146
|
*/
|
|
135
147
|
export async function loadExtensions(ctx, cliExtensions) {
|
|
136
148
|
const specifiers = [];
|
|
137
|
-
// 1. CLI -e / --extensions
|
|
138
149
|
if (cliExtensions) {
|
|
139
150
|
specifiers.push(...cliExtensions);
|
|
140
151
|
}
|
|
141
|
-
// 2. settings.json
|
|
142
152
|
const settings = getSettings();
|
|
143
153
|
if (settings.extensions.length > 0) {
|
|
144
154
|
specifiers.push(...settings.extensions);
|
|
145
155
|
}
|
|
146
|
-
// 3. ~/.agent-sh/extensions/ directory
|
|
147
156
|
const userSpecifiers = await discoverUserExtensions();
|
|
148
157
|
specifiers.push(...userSpecifiers);
|
|
149
|
-
// Deduplicate
|
|
150
158
|
const seen = new Set();
|
|
151
159
|
const unique = specifiers.filter((s) => {
|
|
152
160
|
if (seen.has(s))
|
|
@@ -154,8 +162,7 @@ export async function loadExtensions(ctx, cliExtensions) {
|
|
|
154
162
|
seen.add(s);
|
|
155
163
|
return true;
|
|
156
164
|
});
|
|
157
|
-
|
|
158
|
-
const loaded = await loadSpecifiers(unique, ctx, false, userSpecifiers);
|
|
165
|
+
const loaded = await loadSpecifiers(unique, ctx, false);
|
|
159
166
|
return loaded;
|
|
160
167
|
}
|
|
161
168
|
async function discoverUserExtensions() {
|
|
@@ -188,8 +195,7 @@ async function discoverUserExtensions() {
|
|
|
188
195
|
}
|
|
189
196
|
return specifiers;
|
|
190
197
|
}
|
|
191
|
-
async function loadSpecifiers(specifiers, ctx, bustCache
|
|
192
|
-
const userSet = new Set(userSpecifiers ?? []);
|
|
198
|
+
async function loadSpecifiers(specifiers, ctx, bustCache) {
|
|
193
199
|
const loaded = [];
|
|
194
200
|
for (const specifier of specifiers) {
|
|
195
201
|
try {
|
|
@@ -197,7 +203,6 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
|
|
|
197
203
|
if (TS_EXTS.some((ext) => importPath.endsWith(ext))) {
|
|
198
204
|
await ensureTsSupport(bustCache);
|
|
199
205
|
}
|
|
200
|
-
// Append timestamp query to bust Node's module cache on reload
|
|
201
206
|
if (bustCache) {
|
|
202
207
|
const sep = importPath.includes("?") ? "&" : "?";
|
|
203
208
|
importPath += `${sep}t=${Date.now()}`;
|
|
@@ -212,23 +217,13 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
|
|
|
212
217
|
if (typeof activate === "function") {
|
|
213
218
|
const base = path.basename(specifier).replace(/\.(ts|js|mjs|mts|tsx)$/, "");
|
|
214
219
|
const name = base === "index" ? path.basename(path.dirname(specifier)) : base;
|
|
215
|
-
// Scoped context so /reload can tear user extensions down.
|
|
216
220
|
// Awaiting activate() lets extensions with async setup (e.g.
|
|
217
221
|
// openrouter fetching its model catalog) finish before we move
|
|
218
222
|
// on; a 10s outer timeout in index.ts guards against hangs.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await activate(scoped);
|
|
224
|
-
extensionDisposers.set(name, dispose);
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
const { scoped, dispose } = createScopedContext(ctx, name);
|
|
228
|
-
await activate(scoped);
|
|
229
|
-
// Non-user extensions aren't reloadable, but track for cleanup on shutdown
|
|
230
|
-
extensionDisposers.set(name, dispose);
|
|
231
|
-
}
|
|
223
|
+
extensionDisposers.get(name)?.();
|
|
224
|
+
const { scoped, dispose } = createScopedContext(ctx, name);
|
|
225
|
+
await activate(scoped);
|
|
226
|
+
extensionDisposers.set(name, dispose);
|
|
232
227
|
loaded.push(name);
|
|
233
228
|
}
|
|
234
229
|
}
|
|
@@ -246,7 +241,7 @@ async function loadSpecifiers(specifiers, ctx, bustCache, userSpecifiers) {
|
|
|
246
241
|
*/
|
|
247
242
|
export async function reloadExtensions(ctx) {
|
|
248
243
|
const specifiers = await discoverUserExtensions();
|
|
249
|
-
return loadSpecifiers(specifiers, ctx, true
|
|
244
|
+
return loadSpecifiers(specifiers, ctx, true);
|
|
250
245
|
}
|
|
251
246
|
/**
|
|
252
247
|
* Find an index file in a directory extension.
|
|
@@ -284,15 +279,12 @@ async function resolveSpecifier(specifier) {
|
|
|
284
279
|
// Scoped packages ("@scope/pkg") contain "/" but are npm specifiers,
|
|
285
280
|
// so the "@" prefix takes precedence over the "/" heuristic.
|
|
286
281
|
if (specifier.includes("/") && !specifier.startsWith("@")) {
|
|
287
|
-
// Treat as relative path from cwd
|
|
288
282
|
resolved = path.resolve(process.cwd(), specifier);
|
|
289
283
|
}
|
|
290
284
|
else {
|
|
291
|
-
// Bare specifier — npm package (including @scope/pkg)
|
|
292
285
|
return specifier;
|
|
293
286
|
}
|
|
294
287
|
}
|
|
295
|
-
// If it's a directory, find the index file
|
|
296
288
|
try {
|
|
297
289
|
const stat = await fs.stat(resolved);
|
|
298
290
|
if (stat.isDirectory()) {
|