agent-sh 0.12.23 → 0.12.24
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 +2 -2
- package/dist/core.d.ts +1 -1
- package/dist/core.js +2 -2
- package/dist/event-bus.d.ts +2 -0
- package/dist/extensions/agent-backend.js +1 -2
- package/dist/index.js +45 -11
- package/dist/install.d.ts +10 -0
- package/dist/install.js +205 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ So I built agent-sh. Under the hood it's a normal shell on top of node-pty — y
|
|
|
19
19
|
~ $ > draft a commit message # agent reads your diff and shell history
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
I still use a proper coding harness for serious work — this doesn't replace that. But for the quick stuff in the terminal, I reach for agent-sh almost every day now. The built-in agent is lightweight and good enough for most of what I throw at it, and when it isn't, you can swap in [pi](examples/extensions/pi-bridge/) as the backend
|
|
22
|
+
I still use a proper coding harness for serious work — this doesn't replace that. But for the quick stuff in the terminal, I reach for agent-sh almost every day now. The built-in agent is lightweight and good enough for most of what I throw at it, and when it isn't, you can swap in [pi](examples/extensions/pi-bridge/) as the backend — `agent-sh install pi-bridge` followed by `agent-sh --backend pi`.
|
|
23
23
|
|
|
24
24
|
## Quick Start
|
|
25
25
|
|
|
@@ -95,7 +95,7 @@ Requires Node.js 18+. Currently supports **bash** and **zsh**; other shells (fis
|
|
|
95
95
|
|
|
96
96
|
**Context that just works.** Every query includes your cwd, recent commands, and their output. Run a failing test, type `> fix this`, and agent-sh knows exactly what happened. Context management works like shell history — continuous, persistent across restarts, no sessions to manage. See [Context Management](docs/context-management.md).
|
|
97
97
|
|
|
98
|
-
**Any LLM, any backend.** agent-sh works with any OpenAI-compatible API out of the box. Define multiple providers in settings and switch models at runtime with `/model <name>`. Or swap in a completely different agent — [pi](examples/extensions/pi-bridge/)
|
|
98
|
+
**Any LLM, any backend.** agent-sh works with any OpenAI-compatible API out of the box. Define multiple providers in settings and switch models at runtime with `/model <name>`. Or swap in a completely different agent — `agent-sh install pi-bridge && agent-sh --backend pi` runs [pi](examples/extensions/pi-bridge/) as a drop-in backend.
|
|
99
99
|
|
|
100
100
|
**Extensible by design.** The entire system is built on a typed event bus. Extensions can add custom input modes, content transforms (render LaTeX as images, Mermaid as diagrams), themes, slash commands, or replace the agent backend entirely. The built-in TUI renderer is itself just an extension.
|
|
101
101
|
|
package/dist/core.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface AgentShellCore {
|
|
|
37
37
|
/** Unique id for this agent process; used for shell-marker tagging and lineage tracking. */
|
|
38
38
|
instanceId: string;
|
|
39
39
|
/** Activate the agent backend (call after extensions load). */
|
|
40
|
-
activateBackend(): Promise<void>;
|
|
40
|
+
activateBackend(override?: string): Promise<void>;
|
|
41
41
|
/** Convenience: emit agent:submit and await the response. */
|
|
42
42
|
query(text: string): Promise<string>;
|
|
43
43
|
/** Convenience: emit agent:cancel-request. */
|
package/dist/core.js
CHANGED
|
@@ -102,10 +102,10 @@ export function createCore(config) {
|
|
|
102
102
|
bus,
|
|
103
103
|
handlers,
|
|
104
104
|
instanceId,
|
|
105
|
-
async activateBackend() {
|
|
105
|
+
async activateBackend(override) {
|
|
106
106
|
if (backends.size === 0)
|
|
107
107
|
return;
|
|
108
|
-
const preferred = settings.defaultBackend;
|
|
108
|
+
const preferred = override ?? settings.defaultBackend;
|
|
109
109
|
const name = preferred && backends.has(preferred) ? preferred : backends.keys().next().value;
|
|
110
110
|
await activateByName(name);
|
|
111
111
|
},
|
package/dist/event-bus.d.ts
CHANGED
|
@@ -367,6 +367,8 @@ export interface ShellEvents {
|
|
|
367
367
|
label: string;
|
|
368
368
|
items: string[];
|
|
369
369
|
}>;
|
|
370
|
+
/** Name of the backend being launched. Extensions should gate per-backend sections on this rather than settings.defaultBackend. */
|
|
371
|
+
activeBackend?: string;
|
|
370
372
|
};
|
|
371
373
|
"autocomplete:request": {
|
|
372
374
|
buffer: string;
|
|
@@ -293,8 +293,7 @@ export default function agentBackend(ctx) {
|
|
|
293
293
|
bus.emit("config:changed", {});
|
|
294
294
|
});
|
|
295
295
|
bus.onPipe("banner:collect", (e) => {
|
|
296
|
-
|
|
297
|
-
if (settings.defaultBackend && settings.defaultBackend !== "ash")
|
|
296
|
+
if (e.activeBackend && e.activeBackend !== "ash")
|
|
298
297
|
return e;
|
|
299
298
|
if (loadedExtensionNames.length > 0) {
|
|
300
299
|
e.sections.push({ label: "Extensions", items: [...loadedExtensionNames] });
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadBuiltinExtensions } from "./extensions/index.js";
|
|
|
8
8
|
import { loadExtensions } from "./extension-loader.js";
|
|
9
9
|
import { getSettings } from "./settings.js";
|
|
10
10
|
import { runInit } from "./init.js";
|
|
11
|
+
import { runInstall, runUninstall, runList, suggestBridgeFor } from "./install.js";
|
|
11
12
|
import { PACKAGE_VERSION } from "./utils/package-version.js";
|
|
12
13
|
/**
|
|
13
14
|
* Capture the user's full shell environment.
|
|
@@ -78,7 +79,8 @@ function parseArgs(argv) {
|
|
|
78
79
|
let model;
|
|
79
80
|
let extensions;
|
|
80
81
|
let provider;
|
|
81
|
-
|
|
82
|
+
let backend;
|
|
83
|
+
let shell = process.env.SHELL || "/bin/bash";
|
|
82
84
|
let apiKey = process.env.OPENAI_API_KEY;
|
|
83
85
|
let baseURL = process.env.OPENAI_BASE_URL;
|
|
84
86
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -95,8 +97,11 @@ function parseArgs(argv) {
|
|
|
95
97
|
else if (arg === "--provider" && argv[i + 1]) {
|
|
96
98
|
provider = argv[++i];
|
|
97
99
|
}
|
|
100
|
+
else if (arg === "--backend" && argv[i + 1]) {
|
|
101
|
+
backend = argv[++i];
|
|
102
|
+
}
|
|
98
103
|
else if (arg === "--shell" && argv[i + 1]) {
|
|
99
|
-
|
|
104
|
+
shell = argv[++i];
|
|
100
105
|
}
|
|
101
106
|
else if ((arg === "--extensions" || arg === "-e") && argv[i + 1]) {
|
|
102
107
|
const exts = argv[++i].split(",").map(s => s.trim());
|
|
@@ -110,7 +115,10 @@ function parseArgs(argv) {
|
|
|
110
115
|
console.log(`agent-sh — a shell-first terminal where AI is one keystroke away
|
|
111
116
|
|
|
112
117
|
Usage: agent-sh [options]
|
|
113
|
-
agent-sh init [--force]
|
|
118
|
+
agent-sh init [--force] Scaffold ~/.agent-sh/ (settings, examples, AGENTS.md)
|
|
119
|
+
agent-sh install <spec> [--force] Install an extension (bundled name, file:, npm:, github:)
|
|
120
|
+
agent-sh uninstall <name> Remove an installed extension
|
|
121
|
+
agent-sh list List installed extensions
|
|
114
122
|
|
|
115
123
|
Provider Profiles:
|
|
116
124
|
--provider <name> Use a provider from ~/.agent-sh/settings.json
|
|
@@ -121,6 +129,7 @@ Direct LLM API:
|
|
|
121
129
|
--base-url <url> Base URL for API (or set OPENAI_BASE_URL)
|
|
122
130
|
|
|
123
131
|
General Options:
|
|
132
|
+
--backend <name> Agent backend to launch (e.g. ash, pi); overrides settings.defaultBackend for this session
|
|
124
133
|
--shell <path> Shell to use (default: $SHELL or /bin/bash)
|
|
125
134
|
-e, --extensions Extensions to load (comma-separated, repeatable)
|
|
126
135
|
-h, --help Show this help
|
|
@@ -149,7 +158,7 @@ Inside the shell:
|
|
|
149
158
|
process.exit(0);
|
|
150
159
|
}
|
|
151
160
|
}
|
|
152
|
-
return { shell, model, extensions, apiKey, baseURL, provider };
|
|
161
|
+
return { shell, model, extensions, apiKey, baseURL, provider, backend };
|
|
153
162
|
}
|
|
154
163
|
async function main() {
|
|
155
164
|
// Subcommands — handled before the shell-launch path.
|
|
@@ -158,6 +167,18 @@ async function main() {
|
|
|
158
167
|
runInit({ force: rawArgs.includes("--force") });
|
|
159
168
|
return;
|
|
160
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
|
+
}
|
|
161
182
|
if (process.env.AGENT_SH) {
|
|
162
183
|
console.error("agent-sh: already running inside an agent-sh session (nested sessions are not supported).");
|
|
163
184
|
process.exit(1);
|
|
@@ -280,24 +301,37 @@ async function main() {
|
|
|
280
301
|
console.error("\nagent-sh: no agent backend available.\n\n" +
|
|
281
302
|
" Export OPENROUTER_API_KEY or OPENAI_API_KEY for zero-config launch, or\n" +
|
|
282
303
|
" pass --api-key on the command line, or\n" +
|
|
283
|
-
" run `agent-sh init` for a settings.json template
|
|
284
|
-
"
|
|
304
|
+
" run `agent-sh init` for a settings.json template, or\n" +
|
|
305
|
+
" run `agent-sh install <bridge>` (e.g. pi-bridge, claude-code-bridge) to use a non-ash backend.\n");
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
if (config.backend && !backendNames.includes(config.backend)) {
|
|
309
|
+
shell?.kill();
|
|
310
|
+
const bridge = suggestBridgeFor(config.backend);
|
|
311
|
+
const hint = bridge
|
|
312
|
+
? ` Try: agent-sh install ${bridge}\n`
|
|
313
|
+
: ` Run \`agent-sh install\` to see bundled bridge extensions.\n`;
|
|
314
|
+
console.error(`\nagent-sh: backend "${config.backend}" is not available.\n\n` +
|
|
315
|
+
` Available backends: ${backendNames.join(", ")}\n` +
|
|
316
|
+
hint);
|
|
285
317
|
process.exit(1);
|
|
286
318
|
}
|
|
287
319
|
// No await: banner must out-race the shell's PS1 arriving via PTY.
|
|
288
|
-
core.activateBackend();
|
|
320
|
+
core.activateBackend(config.backend);
|
|
289
321
|
// ── Startup banner ───────────────────────────────────────────
|
|
290
322
|
const settings = getSettings();
|
|
291
323
|
if (settings.startupBanner !== false) {
|
|
292
324
|
const termW = process.stdout.columns || 80;
|
|
293
325
|
const bannerW = Math.min(termW, 60);
|
|
294
326
|
const productName = `${p.accent}${p.bold}agent-sh${p.reset}`;
|
|
295
|
-
const backendName =
|
|
296
|
-
?
|
|
297
|
-
: backendNames
|
|
327
|
+
const backendName = config.backend && backendNames.includes(config.backend)
|
|
328
|
+
? config.backend
|
|
329
|
+
: settings.defaultBackend && backendNames.includes(settings.defaultBackend)
|
|
330
|
+
? settings.defaultBackend
|
|
331
|
+
: backendNames[0];
|
|
298
332
|
let sections = "";
|
|
299
333
|
sections += `\n\n ${p.muted}Backend:${p.reset} ${p.dim}${backendName}${p.reset}`;
|
|
300
|
-
const extSections = bus.emitPipe("banner:collect", { sections: [] }).sections;
|
|
334
|
+
const extSections = bus.emitPipe("banner:collect", { sections: [], activeBackend: backendName }).sections;
|
|
301
335
|
for (const sec of extSections) {
|
|
302
336
|
sections += `\n\n ${p.muted}${sec.label}:${p.reset}`;
|
|
303
337
|
for (const item of sec.items) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface InstallOpts {
|
|
2
|
+
force?: boolean;
|
|
3
|
+
}
|
|
4
|
+
export declare function listBundled(): string[];
|
|
5
|
+
/** Heuristic: a backend named "pi" is typically provided by an extension called "pi-bridge". */
|
|
6
|
+
export declare function suggestBridgeFor(backend: string): string | null;
|
|
7
|
+
export declare function runInstall(spec: string, opts?: InstallOpts): Promise<void>;
|
|
8
|
+
export declare function runUninstall(name: string): Promise<void>;
|
|
9
|
+
export declare function runList(): void;
|
|
10
|
+
export {};
|
package/dist/install.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { CONFIG_DIR, getSettings } from "./settings.js";
|
|
6
|
+
// Kept in sync with extension-loader.ts SCRIPT_EXTS.
|
|
7
|
+
const SCRIPT_EXTS = [".js", ".mjs", ".ts", ".tsx", ".mts"];
|
|
8
|
+
function hasIndexFile(dir) {
|
|
9
|
+
return SCRIPT_EXTS.some((ext) => fs.existsSync(path.join(dir, `index${ext}`)));
|
|
10
|
+
}
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../");
|
|
12
|
+
const BUNDLED_DIR = path.join(PACKAGE_ROOT, "examples/extensions");
|
|
13
|
+
const EXT_DIR = path.join(CONFIG_DIR, "extensions");
|
|
14
|
+
export function listBundled() {
|
|
15
|
+
if (!fs.existsSync(BUNDLED_DIR))
|
|
16
|
+
return [];
|
|
17
|
+
return fs.readdirSync(BUNDLED_DIR).map((n) => n.replace(/\.(ts|js|mjs)$/, ""));
|
|
18
|
+
}
|
|
19
|
+
/** Heuristic: a backend named "pi" is typically provided by an extension called "pi-bridge". */
|
|
20
|
+
export function suggestBridgeFor(backend) {
|
|
21
|
+
const candidate = `${backend}-bridge`;
|
|
22
|
+
return listBundled().includes(candidate) ? candidate : null;
|
|
23
|
+
}
|
|
24
|
+
const bundledResolver = {
|
|
25
|
+
resolve: async (spec) => {
|
|
26
|
+
const candidates = [
|
|
27
|
+
{ p: path.join(BUNDLED_DIR, spec), name: spec },
|
|
28
|
+
{ p: path.join(BUNDLED_DIR, `${spec}.ts`), name: `${spec}.ts` },
|
|
29
|
+
{ p: path.join(BUNDLED_DIR, `${spec}.js`), name: `${spec}.js` },
|
|
30
|
+
];
|
|
31
|
+
for (const c of candidates) {
|
|
32
|
+
if (fs.existsSync(c.p)) {
|
|
33
|
+
const isDirectory = fs.statSync(c.p).isDirectory();
|
|
34
|
+
return { sourcePath: c.p, name: c.name, isDirectory };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const available = listBundled();
|
|
38
|
+
throw new Error(`No bundled extension named "${spec}".\n\n` +
|
|
39
|
+
`Available:\n${available.map((n) => ` ${n}`).join("\n")}`);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const npmResolver = {
|
|
43
|
+
canHandle: (spec) => spec.startsWith("npm:"),
|
|
44
|
+
resolve: async () => {
|
|
45
|
+
throw new Error("npm: source is not yet implemented");
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const githubResolver = {
|
|
49
|
+
canHandle: (spec) => spec.startsWith("github:") || spec.startsWith("https://github.com/"),
|
|
50
|
+
resolve: async () => {
|
|
51
|
+
throw new Error("github: source is not yet implemented");
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const fileResolver = {
|
|
55
|
+
canHandle: (spec) => spec.startsWith("file:") || spec.startsWith("/") || spec.startsWith("./") || spec.startsWith("../"),
|
|
56
|
+
resolve: async (spec) => {
|
|
57
|
+
const raw = spec.startsWith("file:") ? spec.slice("file:".length) : spec;
|
|
58
|
+
const abs = path.resolve(raw);
|
|
59
|
+
if (!fs.existsSync(abs))
|
|
60
|
+
throw new Error(`Path does not exist: ${abs}`);
|
|
61
|
+
const isDirectory = fs.statSync(abs).isDirectory();
|
|
62
|
+
return { sourcePath: abs, name: path.basename(abs), isDirectory };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const PREFIX_RESOLVERS = [npmResolver, githubResolver, fileResolver];
|
|
66
|
+
function pickResolver(spec) {
|
|
67
|
+
for (const r of PREFIX_RESOLVERS)
|
|
68
|
+
if (r.canHandle?.(spec))
|
|
69
|
+
return r;
|
|
70
|
+
return bundledResolver;
|
|
71
|
+
}
|
|
72
|
+
function maybeNpmInstall(target) {
|
|
73
|
+
const pkgJson = path.join(target, "package.json");
|
|
74
|
+
if (!fs.existsSync(pkgJson))
|
|
75
|
+
return;
|
|
76
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJson, "utf-8"));
|
|
77
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) };
|
|
78
|
+
if (Object.keys(deps).length === 0)
|
|
79
|
+
return;
|
|
80
|
+
if (fs.existsSync(path.join(target, "node_modules")))
|
|
81
|
+
return;
|
|
82
|
+
console.log(`Running npm install in ${target}...`);
|
|
83
|
+
const result = spawnSync("npm", ["install", "--no-audit", "--no-fund"], {
|
|
84
|
+
cwd: target,
|
|
85
|
+
stdio: "inherit",
|
|
86
|
+
});
|
|
87
|
+
if (result.status !== 0) {
|
|
88
|
+
throw new Error(`npm install failed in ${target}; run it manually.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function runInstall(spec, opts = {}) {
|
|
92
|
+
if (!spec) {
|
|
93
|
+
console.error("Usage: agent-sh install <name|file:|npm:|github:> [--force]\n\n" +
|
|
94
|
+
"Bundled extensions:\n" +
|
|
95
|
+
listBundled()
|
|
96
|
+
.map((n) => ` ${n}`)
|
|
97
|
+
.join("\n"));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
fs.mkdirSync(EXT_DIR, { recursive: true });
|
|
101
|
+
let resolved;
|
|
102
|
+
try {
|
|
103
|
+
resolved = await pickResolver(spec).resolve(spec);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`agent-sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
const target = path.join(EXT_DIR, resolved.name);
|
|
110
|
+
if (fs.lstatSync(target, { throwIfNoEntry: false })) {
|
|
111
|
+
if (!opts.force) {
|
|
112
|
+
console.error(`agent-sh: ${target} already exists (pass --force to overwrite)`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
if (resolved.isDirectory) {
|
|
118
|
+
fs.cpSync(resolved.sourcePath, target, { recursive: true });
|
|
119
|
+
try {
|
|
120
|
+
maybeNpmInstall(target);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
console.error(`agent-sh: ${err instanceof Error ? err.message : String(err)}`);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
fs.copyFileSync(resolved.sourcePath, target);
|
|
129
|
+
}
|
|
130
|
+
console.log(`Installed: ${resolved.name} -> ${target}`);
|
|
131
|
+
}
|
|
132
|
+
export async function runUninstall(name) {
|
|
133
|
+
if (!name) {
|
|
134
|
+
console.error("Usage: agent-sh uninstall <name>");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const target = path.join(EXT_DIR, name);
|
|
138
|
+
// Refuse path-traversal: target must sit directly under EXT_DIR.
|
|
139
|
+
const resolvedTarget = path.resolve(target);
|
|
140
|
+
const resolvedExtDir = path.resolve(EXT_DIR);
|
|
141
|
+
if (!resolvedTarget.startsWith(resolvedExtDir + path.sep)) {
|
|
142
|
+
console.error(`agent-sh: refusing to uninstall outside ${EXT_DIR}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
if (!fs.lstatSync(target, { throwIfNoEntry: false })) {
|
|
146
|
+
console.error(`agent-sh: not installed: ${name}`);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
150
|
+
console.log(`Uninstalled: ${name}`);
|
|
151
|
+
}
|
|
152
|
+
function listFromExtDir(disabled) {
|
|
153
|
+
if (!fs.existsSync(EXT_DIR))
|
|
154
|
+
return [];
|
|
155
|
+
const dirents = fs.readdirSync(EXT_DIR, { withFileTypes: true });
|
|
156
|
+
const out = [];
|
|
157
|
+
for (const d of dirents) {
|
|
158
|
+
if (d.name.startsWith("."))
|
|
159
|
+
continue;
|
|
160
|
+
const nameForDisable = d.name.replace(/\.[^.]+$/, "");
|
|
161
|
+
if (disabled.has(nameForDisable))
|
|
162
|
+
continue;
|
|
163
|
+
const full = path.join(EXT_DIR, d.name);
|
|
164
|
+
let isDir = d.isDirectory();
|
|
165
|
+
if (d.isSymbolicLink()) {
|
|
166
|
+
try {
|
|
167
|
+
isDir = fs.statSync(full).isDirectory();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (isDir) {
|
|
174
|
+
if (!hasIndexFile(full))
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
else if (!SCRIPT_EXTS.some((ext) => d.name.endsWith(ext))) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const detail = d.isSymbolicLink() ? `-> ${fs.readlinkSync(full)}` : undefined;
|
|
181
|
+
out.push({ name: d.name, source: "extensions dir", detail });
|
|
182
|
+
}
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
function listFromSettings(disabled) {
|
|
186
|
+
const specs = getSettings().extensions ?? [];
|
|
187
|
+
return specs
|
|
188
|
+
.filter((s) => !disabled.has(s.replace(/\.[^.]+$/, "")))
|
|
189
|
+
.map((s) => ({ name: s, source: "settings.json" }));
|
|
190
|
+
}
|
|
191
|
+
export function runList() {
|
|
192
|
+
const disabled = new Set(getSettings().disabledExtensions ?? []);
|
|
193
|
+
const items = [...listFromExtDir(disabled), ...listFromSettings(disabled)];
|
|
194
|
+
if (items.length === 0) {
|
|
195
|
+
console.log("No extensions installed.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const nameWidth = Math.max(...items.map((i) => i.name.length));
|
|
199
|
+
console.log("Installed extensions:");
|
|
200
|
+
for (const item of items) {
|
|
201
|
+
const padded = item.name.padEnd(nameWidth);
|
|
202
|
+
const detail = item.detail ? ` ${item.detail}` : "";
|
|
203
|
+
console.log(` ${padded} (${item.source})${detail}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -100,6 +100,8 @@ export interface AgentShellConfig {
|
|
|
100
100
|
baseURL?: string;
|
|
101
101
|
/** Named provider to use from settings.json. */
|
|
102
102
|
provider?: string;
|
|
103
|
+
/** Override settings.defaultBackend for this session only (does not persist). */
|
|
104
|
+
backend?: string;
|
|
103
105
|
/** Conversation history backend. Defaults to the on-disk HistoryFile. */
|
|
104
106
|
history?: HistoryAdapter;
|
|
105
107
|
}
|