@alisio/core 0.1.0-alpha.3 → 0.1.0-alpha.6
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/dist/application.d.ts +22 -2
- package/dist/application.js +116 -7
- package/dist/config.d.ts +15 -1
- package/dist/config.js +85 -8
- package/dist/core/compaction.d.ts +36 -1
- package/dist/core/compaction.js +66 -17
- package/dist/core/runner.d.ts +20 -1
- package/dist/core/runner.js +43 -8
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/mcp/connector.d.ts +2 -0
- package/dist/mcp/connector.js +9 -1
- package/dist/plugins/host.js +7 -1
- package/dist/plugins/install.d.ts +100 -0
- package/dist/plugins/install.js +322 -0
- package/dist/resources/context.js +1 -1
- package/dist/runtime/modules.d.ts +3 -1
- package/dist/runtime/modules.js +9 -2
- package/dist/runtime/process.d.ts +9 -0
- package/dist/runtime/process.js +18 -0
- package/dist/tools/standard.js +14 -3
- package/dist/version.d.ts +7 -0
- package/dist/version.js +26 -0
- package/package.json +2 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installer for npm plugin packages: `alisio install npm:<package>[@<version>]`.
|
|
3
|
+
*
|
|
4
|
+
* One shared routine (`installPlugin`) backs both the CLI command (`cliInstall`) and the
|
|
5
|
+
* host-owned `plugin_install` tool for the agent, so the two can never drift apart. The install
|
|
6
|
+
* target is the GLOBAL plugins directory `<configHome>/plugins` (default `~/.config/alisio/plugins`),
|
|
7
|
+
* installed with `npm install --prefix`; the package NAME is persisted in the global configuration
|
|
8
|
+
* `plugins` array (never the resolved filesystem path), keeping entries portable. Loading the plugin
|
|
9
|
+
* still follows the existing executable-plugin policy: it runs in-process wherever it is loaded and
|
|
10
|
+
* is disabled entirely under `--read-only`.
|
|
11
|
+
*/
|
|
12
|
+
import { chmod, mkdir, open, readdir, rename, unlink } from "node:fs/promises";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
import { textResult } from "@alisio/sdk";
|
|
15
|
+
import { configHome } from "../config.js";
|
|
16
|
+
import { readJson } from "../runtime/fs.js";
|
|
17
|
+
import { PLUGIN_KEYWORD } from "../runtime/modules.js";
|
|
18
|
+
import { runProcess } from "../runtime/process.js";
|
|
19
|
+
/** npm package name: scope (optional) followed by the name; both start with an alphanumeric. */
|
|
20
|
+
const PACKAGE_PART = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
21
|
+
const NAME_RE = new RegExp(`^(?:@${PACKAGE_PART}\\/)?${PACKAGE_PART}$`);
|
|
22
|
+
/** Version specifier: npm's `@<version>` part, restricted to the same safe character set. */
|
|
23
|
+
const VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
24
|
+
/**
|
|
25
|
+
* Validates an install spec without touching the network: `npm:<package>[@<version>]` (canonical),
|
|
26
|
+
* a bare package name (same resolution), and nothing else. Rejects shell metacharacters, `..` /
|
|
27
|
+
* absolute paths, and unknown prefixes (`registry:`, `git:`, `file:`, URLs) with a clear error.
|
|
28
|
+
*/
|
|
29
|
+
export function parsePluginSpec(spec) {
|
|
30
|
+
if (!spec || !spec.trim())
|
|
31
|
+
throw new Error("Plugin spec is empty: use npm:<package>[@<version>] or a bare package name, for example npm:plugin-openrouter");
|
|
32
|
+
const trimmed = spec.trim();
|
|
33
|
+
let rest = trimmed;
|
|
34
|
+
if (trimmed.startsWith("npm:"))
|
|
35
|
+
rest = trimmed.slice(4);
|
|
36
|
+
else {
|
|
37
|
+
const colon = trimmed.indexOf(":");
|
|
38
|
+
if (colon >= 0)
|
|
39
|
+
throw new Error(`Unsupported plugin spec prefix "${trimmed.slice(0, colon)}:": only npm:<package>[@<version>] ` +
|
|
40
|
+
`or a bare package name is supported (registry:/git:/file:/http: specs are not)` +
|
|
41
|
+
(trimmed.startsWith("registry:")
|
|
42
|
+
? "; pass the package name directly instead of a registry URL"
|
|
43
|
+
: ""));
|
|
44
|
+
}
|
|
45
|
+
if (!rest)
|
|
46
|
+
throw new Error(`Plugin spec "${spec}" has no package name`);
|
|
47
|
+
const at = rest.lastIndexOf("@");
|
|
48
|
+
let name = rest;
|
|
49
|
+
let version;
|
|
50
|
+
if (at > 0) {
|
|
51
|
+
name = rest.slice(0, at);
|
|
52
|
+
version = rest.slice(at + 1);
|
|
53
|
+
}
|
|
54
|
+
if (name.length > 214)
|
|
55
|
+
throw new Error(`Invalid plugin spec "${spec}": package name is too long`);
|
|
56
|
+
if (!NAME_RE.test(name) || name.includes(".."))
|
|
57
|
+
throw new Error(`Invalid plugin package name "${name}" in spec "${spec}": allowed characters are letters, ` +
|
|
58
|
+
`digits, dot, underscore and dash, with "/" only for a scoped name and "@" only for a version; ` +
|
|
59
|
+
`names may not start with "." or "_" and may not contain ".."`);
|
|
60
|
+
if (version !== undefined && !VERSION_RE.test(version))
|
|
61
|
+
throw new Error(`Invalid plugin version "${version}" in spec "${spec}": versions may contain only letters, ` +
|
|
62
|
+
`digits, dot, underscore and dash (for example 1.2.3, 1.2.3-beta.1 or latest)`);
|
|
63
|
+
return { name, version };
|
|
64
|
+
}
|
|
65
|
+
/** The npm argument for a spec: `name`, `name@<version>` or `name@latest` for `--update`. */
|
|
66
|
+
export function npmInstallTarget(name, version, update = false) {
|
|
67
|
+
return update ? `${name}@latest` : version ? `${name}@${version}` : name;
|
|
68
|
+
}
|
|
69
|
+
export const INSTALL_TIMEOUT_MS = 300_000;
|
|
70
|
+
const defaultRunner = (command, args, options) => runProcess(command, args, { ...options, timeoutMs: INSTALL_TIMEOUT_MS, maxBytes: 64_000 });
|
|
71
|
+
/**
|
|
72
|
+
* Redacts probable secrets from npm output (auth tokens, JWTs, bearer credentials, userinfo in
|
|
73
|
+
* URLs) and caps the size, so a failing install never echoes credentials to the user.
|
|
74
|
+
*/
|
|
75
|
+
export function sanitizeNpmError(text, maxLength = 4000) {
|
|
76
|
+
const MAX = Math.max(1, maxLength);
|
|
77
|
+
let cleaned = text.replace(/(eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]+|\bbearer\s+[A-Za-z0-9._~+/-]{12,}=*\b|\b(?:npm_?authToken|_authToken|NODE_AUTH_TOKEN|NPM_TOKEN|authToken|password|secret)\b\s*(?:=|:)\s*\S+|\b(?:npm_?authToken|_authToken|NODE_AUTH_TOKEN|NPM_TOKEN|authToken|password|secret)\b\s+\S+)/gi, "[redacted]");
|
|
78
|
+
cleaned = cleaned.replace(/([a-z][a-z0-9+.-]*):\/\/[^@\s/]+@/gi, "$1://[redacted]@");
|
|
79
|
+
if (cleaned.length > MAX)
|
|
80
|
+
cleaned = `${cleaned.slice(0, MAX)}\n…[npm output truncated]`;
|
|
81
|
+
return cleaned;
|
|
82
|
+
}
|
|
83
|
+
function npmInstallError(name, version, update, pluginsDir, result) {
|
|
84
|
+
const detail = sanitizeNpmError([result.stdout, result.stderr].filter(Boolean).join("\n"));
|
|
85
|
+
const retry = `npm install --prefix ${pluginsDir} ${npmInstallTarget(name, version, update)}`;
|
|
86
|
+
return new Error(`npm install failed for plugin "${name}" (exit ${result.exitCode}).\n${detail || "no output"}\n` +
|
|
87
|
+
`Retry with: ${retry}\n(or: alisio install npm:${npmInstallTarget(name, version)})`);
|
|
88
|
+
}
|
|
89
|
+
/** Atomically adds `packageName` to the global configuration `plugins` array, preserving all other fields. */
|
|
90
|
+
async function persistGlobalPluginEntry(configHomeDir, packageName) {
|
|
91
|
+
const file = join(configHomeDir, "config.json");
|
|
92
|
+
const existing = await readJson(file);
|
|
93
|
+
let raw;
|
|
94
|
+
if (existing === undefined)
|
|
95
|
+
raw = {};
|
|
96
|
+
else if (typeof existing === "object" && !Array.isArray(existing))
|
|
97
|
+
raw = existing;
|
|
98
|
+
else
|
|
99
|
+
throw new Error(`Global Alisio configuration must be a JSON object: ${file}`);
|
|
100
|
+
const current = Array.isArray(raw.plugins) ? raw.plugins : [];
|
|
101
|
+
if (!current.includes(packageName))
|
|
102
|
+
raw.plugins = [...current, packageName];
|
|
103
|
+
raw.schemaVersion ??= 1;
|
|
104
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
105
|
+
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
106
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
107
|
+
try {
|
|
108
|
+
await handle.writeFile(`${JSON.stringify(raw, null, 2)}\n`, "utf8");
|
|
109
|
+
await handle.sync();
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await handle.close();
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
await rename(temporary, file);
|
|
116
|
+
await chmod(file, 0o600);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
await unlink(temporary).catch(() => { });
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
return file;
|
|
123
|
+
}
|
|
124
|
+
/** The shared install routine used by both the CLI command and the `plugin_install` tool. */
|
|
125
|
+
export async function installPlugin(input) {
|
|
126
|
+
const parsed = parsePluginSpec(input.spec);
|
|
127
|
+
if (input.readOnly)
|
|
128
|
+
throw new Error(`Installing npm plugin "${parsed.name}" is unavailable under --read-only: installation runs npm and writes your global configuration. Run without --read-only.`);
|
|
129
|
+
const home = resolve(input.configHome ?? configHome());
|
|
130
|
+
const pluginsDir = join(home, "plugins");
|
|
131
|
+
// npm creates the prefix directory itself, but the subprocess cwd must already exist for spawn.
|
|
132
|
+
await mkdir(pluginsDir, { recursive: true, mode: 0o700 });
|
|
133
|
+
const packageDir = join(pluginsDir, "node_modules", parsed.name);
|
|
134
|
+
const installedManifest = (await readJson(join(packageDir, "package.json")));
|
|
135
|
+
const alreadyInstalled = !!installedManifest?.version;
|
|
136
|
+
let updated = false;
|
|
137
|
+
if (!alreadyInstalled || input.update) {
|
|
138
|
+
const signal = input.signal ?? AbortSignal.timeout(INSTALL_TIMEOUT_MS);
|
|
139
|
+
const run = input.runner ?? defaultRunner;
|
|
140
|
+
const npmResult = await run("npm", [
|
|
141
|
+
"install",
|
|
142
|
+
"--prefix",
|
|
143
|
+
pluginsDir,
|
|
144
|
+
npmInstallTarget(parsed.name, parsed.version, input.update),
|
|
145
|
+
], { cwd: pluginsDir, signal });
|
|
146
|
+
if (npmResult.exitCode !== 0)
|
|
147
|
+
throw npmInstallError(parsed.name, parsed.version, input.update, pluginsDir, npmResult);
|
|
148
|
+
updated = !!input.update && alreadyInstalled;
|
|
149
|
+
}
|
|
150
|
+
const manifest = (await readJson(join(packageDir, "package.json")));
|
|
151
|
+
const installedVersion = manifest?.version ?? "unknown";
|
|
152
|
+
const configFile = await persistGlobalPluginEntry(home, parsed.name);
|
|
153
|
+
return {
|
|
154
|
+
packageName: parsed.name,
|
|
155
|
+
installedVersion,
|
|
156
|
+
path: packageDir,
|
|
157
|
+
configEntry: parsed.name,
|
|
158
|
+
configFile,
|
|
159
|
+
pluginsDir,
|
|
160
|
+
alreadyInstalled,
|
|
161
|
+
updated,
|
|
162
|
+
trustRemark: pluginTrustRemark(parsed.name),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Final remark about loading and project trust, printed by the CLI and returned by the tool. */
|
|
166
|
+
export function pluginTrustRemark(name) {
|
|
167
|
+
return (`The plugin "${name}" is installed globally for your user; it loads as trusted personal code ` +
|
|
168
|
+
`wherever global plugins load — use \`alisio --trust-project\` or accept the one-time trust prompt ` +
|
|
169
|
+
`in a project so its configuration and project plugins load too. \`--read-only\` keeps the plugin ` +
|
|
170
|
+
`from loading. Run \`alisio plugins list\` to inspect.`);
|
|
171
|
+
}
|
|
172
|
+
const PREINSTALL_WARNING = `Warning: "npm install" may run the package's lifecycle scripts (preinstall, install, postinstall) ` +
|
|
173
|
+
`with your user privileges. Alisio does not sandbox plugin installation or plugin code; the plugin ` +
|
|
174
|
+
`runs in-process if it is loaded. Only install packages you trust.`;
|
|
175
|
+
export async function cliInstall(options) {
|
|
176
|
+
// Validate the spec before any network operation or prompt.
|
|
177
|
+
const parsed = parsePluginSpec(options.spec);
|
|
178
|
+
if (options.readOnly)
|
|
179
|
+
throw new Error(`Refusing to install npm plugin "${parsed.name}" under --read-only: installing runs npm and writes your global configuration. Run the command without --read-only.`);
|
|
180
|
+
if (options.yes) {
|
|
181
|
+
// Explicit opt-in: no warning, no prompt.
|
|
182
|
+
}
|
|
183
|
+
else if (options.interactive) {
|
|
184
|
+
process.stderr.write(`\n${PREINSTALL_WARNING}\n`);
|
|
185
|
+
const confirmed = await (options.confirm ?? defaultConfirm)();
|
|
186
|
+
if (!confirmed)
|
|
187
|
+
throw new Error(`Install of plugin "${parsed.name}" cancelled; nothing was installed or configured.`);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// Headless (including --json): never prompt, fail with an actionable error.
|
|
191
|
+
throw new Error(`Refusing to install npm plugin "${parsed.name}" without confirmation: npm install may run ` +
|
|
192
|
+
`lifecycle scripts from the package. Re-run with --yes (or --trust-plugin) to confirm, for ` +
|
|
193
|
+
`example: alisio install npm:${npmInstallTarget(parsed.name, parsed.version)} --yes`);
|
|
194
|
+
}
|
|
195
|
+
const result = await installPlugin({
|
|
196
|
+
spec: options.spec,
|
|
197
|
+
configHome: options.configHome,
|
|
198
|
+
update: options.update,
|
|
199
|
+
signal: options.signal,
|
|
200
|
+
runner: options.runner,
|
|
201
|
+
});
|
|
202
|
+
printInstallResult(result, { json: !!options.json });
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
async function defaultConfirm() {
|
|
206
|
+
const { createInterface } = await import("node:readline/promises");
|
|
207
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
208
|
+
try {
|
|
209
|
+
const answer = (await rl.question("Install this plugin package globally? [y/N] "))
|
|
210
|
+
.trim()
|
|
211
|
+
.toLowerCase();
|
|
212
|
+
return answer === "y" || answer === "yes";
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
rl.close();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function printInstallResult(result, options = {}) {
|
|
219
|
+
if (options.json) {
|
|
220
|
+
process.stdout.write(`${JSON.stringify({
|
|
221
|
+
packageName: result.packageName,
|
|
222
|
+
installedVersion: result.installedVersion,
|
|
223
|
+
path: result.path,
|
|
224
|
+
configEntry: result.configEntry,
|
|
225
|
+
configFile: result.configFile,
|
|
226
|
+
alreadyInstalled: result.alreadyInstalled,
|
|
227
|
+
updated: result.updated,
|
|
228
|
+
trustRemark: result.trustRemark,
|
|
229
|
+
}, null, 2)}\n`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (result.alreadyInstalled && !result.updated)
|
|
233
|
+
console.log(`Plugin "${result.packageName}" is already installed (v${result.installedVersion}). Use --update to refresh it to the latest version and keep its name.`);
|
|
234
|
+
else if (result.updated)
|
|
235
|
+
console.log(`Updated plugin "${result.packageName}" to v${result.installedVersion}.`);
|
|
236
|
+
else
|
|
237
|
+
console.log(`Installed plugin "${result.packageName}" v${result.installedVersion}.`);
|
|
238
|
+
console.log(` path: ${result.path}`);
|
|
239
|
+
console.log(` config: "plugins" entry "${result.configEntry}" in ${result.configFile}`);
|
|
240
|
+
console.log(result.trustRemark);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Registers the host-owned `plugin_install` tool (agent capability). It has process effect so it
|
|
244
|
+
* goes through the existing write/process/external permission gate (TUI approval or
|
|
245
|
+
* `--allow-process`; never available under `--read-only`, where it is not registered at all).
|
|
246
|
+
*/
|
|
247
|
+
export function registerPluginInstallTool(registry, options = {}) {
|
|
248
|
+
if (options.readOnly)
|
|
249
|
+
return;
|
|
250
|
+
const tool = {
|
|
251
|
+
name: "plugin_install",
|
|
252
|
+
effect: "process",
|
|
253
|
+
description: "Install an npm plugin package into Alisio's global plugins directory " +
|
|
254
|
+
"(<configHome>/plugins, for example ~/.config/alisio/plugins) and add its npm name to the " +
|
|
255
|
+
'global configuration "plugins" array so it can load across projects. Accepts "npm:<package>' +
|
|
256
|
+
"[@<version>]\" or a bare package name. npm may run the package's lifecycle scripts with your " +
|
|
257
|
+
"privileges, so this tool goes through the process permission gate like any subprocess; the " +
|
|
258
|
+
"installed plugin runs in-process wherever it is loaded and is disabled under --read-only.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
properties: { spec: { type: "string", minLength: 1 } },
|
|
262
|
+
required: ["spec"],
|
|
263
|
+
additionalProperties: false,
|
|
264
|
+
},
|
|
265
|
+
async execute(input, context) {
|
|
266
|
+
const result = await installPlugin({
|
|
267
|
+
spec: String(input.spec),
|
|
268
|
+
configHome: options.configHome,
|
|
269
|
+
runner: options.runner,
|
|
270
|
+
signal: context.signal,
|
|
271
|
+
});
|
|
272
|
+
return textResult(JSON.stringify({
|
|
273
|
+
packageName: result.packageName,
|
|
274
|
+
installedVersion: result.installedVersion,
|
|
275
|
+
path: result.path,
|
|
276
|
+
configEntry: result.configEntry,
|
|
277
|
+
configFile: result.configFile,
|
|
278
|
+
alreadyInstalled: result.alreadyInstalled,
|
|
279
|
+
updated: result.updated,
|
|
280
|
+
trustRemark: result.trustRemark,
|
|
281
|
+
}));
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
registry.register(tool);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Lists npm packages installed under `<root>/node_modules` that declare the `alisio-plugin`
|
|
288
|
+
* keyword — the same packages `plugins`-array entries resolve to. Used by `alisio plugins list`
|
|
289
|
+
* so an installed package shows up alongside file/directory plugins.
|
|
290
|
+
*/
|
|
291
|
+
export async function installedNpmPlugins(root) {
|
|
292
|
+
const packagesDir = join(root, "node_modules");
|
|
293
|
+
let entries;
|
|
294
|
+
try {
|
|
295
|
+
entries = await readdir(packagesDir, { withFileTypes: true });
|
|
296
|
+
}
|
|
297
|
+
catch (e) {
|
|
298
|
+
if (e.code === "ENOENT")
|
|
299
|
+
return [];
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
302
|
+
const result = [];
|
|
303
|
+
const consider = async (dir) => {
|
|
304
|
+
const manifest = (await readJson(join(dir, "package.json")));
|
|
305
|
+
if (manifest && Array.isArray(manifest.keywords) && manifest.keywords.includes(PLUGIN_KEYWORD))
|
|
306
|
+
result.push(dir);
|
|
307
|
+
};
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
310
|
+
continue;
|
|
311
|
+
if (entry.name.startsWith("@")) {
|
|
312
|
+
const scoped = await readdir(join(packagesDir, entry.name), { withFileTypes: true }).catch(() => []);
|
|
313
|
+
for (const scope of scoped)
|
|
314
|
+
if (scope.isDirectory())
|
|
315
|
+
await consider(join(packagesDir, entry.name, scope.name));
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
await consider(join(packagesDir, entry.name));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return result.sort();
|
|
322
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - Global: `<config home>/AGENTS.override.md` or `<config home>/AGENTS.md`.
|
|
5
5
|
* - Project: one file per directory from the workspace root down to cwd, choosing
|
|
6
|
-
* AGENTS.override.md > AGENTS.md > AGENT.md (legacy alias
|
|
6
|
+
* AGENTS.override.md > AGENTS.md > AGENT.md (legacy alias from the earliest alphas) >
|
|
7
7
|
* CLAUDE.md (only with `claudeMdFallback`). Concatenated root-first so the closest is last.
|
|
8
8
|
* - Nested files below other directories are attached lazily when tools touch paths under them,
|
|
9
9
|
* once per session per file (re-attached if the file changes).
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export declare const PLUGIN_KEYWORD = "alisio-plugin";
|
|
2
2
|
/** Paths start with `.`, are absolute, use `\`, end in a JS/TS extension or contain `/` unscoped. */
|
|
3
3
|
export declare function isPathSpec(spec: string): boolean;
|
|
4
|
-
/** Global package roots: NODE_PATH entries
|
|
4
|
+
/** Global package roots: NODE_PATH entries, the prefix of the running Node/npm, and the
|
|
5
|
+
* Alisio global plugins directory (`<configHome>/plugins`), where `alisio install` puts npm
|
|
6
|
+
* plugin packages. */
|
|
5
7
|
export declare function defaultGlobalRoots(): string[];
|
|
6
8
|
/**
|
|
7
9
|
* Returns the file to import (or the directory holding an alisio-plugin.json manifest).
|
package/dist/runtime/modules.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Resolution of explicitly trusted plugins given as a path or an npm package name. */
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { configHome } from "../config.js";
|
|
3
4
|
import { readJson } from "./fs.js";
|
|
4
5
|
export const PLUGIN_KEYWORD = "alisio-plugin";
|
|
5
6
|
/** Paths start with `.`, are absolute, use `\`, end in a JS/TS extension or contain `/` unscoped. */
|
|
@@ -11,7 +12,9 @@ export function isPathSpec(spec) {
|
|
|
11
12
|
/\.(?:[cm]?[jt]s|tsx|jsx)$/.test(spec) ||
|
|
12
13
|
(!spec.startsWith("@") && spec.includes("/")));
|
|
13
14
|
}
|
|
14
|
-
/** Global package roots: NODE_PATH entries
|
|
15
|
+
/** Global package roots: NODE_PATH entries, the prefix of the running Node/npm, and the
|
|
16
|
+
* Alisio global plugins directory (`<configHome>/plugins`), where `alisio install` puts npm
|
|
17
|
+
* plugin packages. */
|
|
15
18
|
export function defaultGlobalRoots() {
|
|
16
19
|
const prefix = process.env.npm_config_prefix ?? process.env.PREFIX ?? dirname(dirname(process.execPath));
|
|
17
20
|
return [
|
|
@@ -21,6 +24,7 @@ export function defaultGlobalRoots() {
|
|
|
21
24
|
process.platform === "win32"
|
|
22
25
|
? join(prefix, "node_modules")
|
|
23
26
|
: join(prefix, "lib", "node_modules"),
|
|
27
|
+
join(configHome(), "plugins"),
|
|
24
28
|
];
|
|
25
29
|
}
|
|
26
30
|
function entryOf(manifest) {
|
|
@@ -58,8 +62,11 @@ export async function resolvePluginSpec(spec, options) {
|
|
|
58
62
|
if (dirname(dir) === dir)
|
|
59
63
|
break;
|
|
60
64
|
}
|
|
61
|
-
for (const rootDir of options.globalRoots ?? defaultGlobalRoots())
|
|
65
|
+
for (const rootDir of options.globalRoots ?? defaultGlobalRoots()) {
|
|
62
66
|
candidates.push(join(rootDir, spec));
|
|
67
|
+
// `alisio install` writes npm packages under <root>/node_modules/<spec>.
|
|
68
|
+
candidates.push(join(rootDir, "node_modules", spec));
|
|
69
|
+
}
|
|
63
70
|
for (const dir of candidates) {
|
|
64
71
|
searched.push(dir);
|
|
65
72
|
const manifest = (await readJson(join(dir, "package.json")));
|
|
@@ -4,6 +4,15 @@ export interface ProcessResult {
|
|
|
4
4
|
exitCode: number;
|
|
5
5
|
truncated: boolean;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Detect a spawn failure caused by a missing executable (`ENOENT`), so tools can explain how to
|
|
9
|
+
* install the dependency instead of surfacing the raw OS error.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isMissingCommand(error: unknown): boolean;
|
|
12
|
+
/** Per-platform install commands for the ripgrep dependency. */
|
|
13
|
+
export declare const RIPGREP_INSTALL_HINT: string;
|
|
14
|
+
/** Human-readable detection status for `alisio doctor`. */
|
|
15
|
+
export declare function ripgrepDiagnostic(found: boolean): string | undefined;
|
|
7
16
|
export declare function runProcess(command: string, args: string[], options: {
|
|
8
17
|
cwd: string;
|
|
9
18
|
signal: AbortSignal;
|
package/dist/runtime/process.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Detect a spawn failure caused by a missing executable (`ENOENT`), so tools can explain how to
|
|
4
|
+
* install the dependency instead of surfacing the raw OS error.
|
|
5
|
+
*/
|
|
6
|
+
export function isMissingCommand(error) {
|
|
7
|
+
return (!!error &&
|
|
8
|
+
typeof error === "object" &&
|
|
9
|
+
"code" in error &&
|
|
10
|
+
error.code === "ENOENT");
|
|
11
|
+
}
|
|
12
|
+
/** Per-platform install commands for the ripgrep dependency. */
|
|
13
|
+
export const RIPGREP_INSTALL_HINT = "Install ripgrep (rg): `sudo apt install ripgrep` (Debian/Ubuntu), " +
|
|
14
|
+
"`brew install ripgrep` (macOS), `winget install BurntSushi.ripgrep.MSVC` or " +
|
|
15
|
+
"`scoop install ripgrep` (Windows). It powers the search_text and list_files tools.";
|
|
16
|
+
/** Human-readable detection status for `alisio doctor`. */
|
|
17
|
+
export function ripgrepDiagnostic(found) {
|
|
18
|
+
return found ? undefined : RIPGREP_INSTALL_HINT;
|
|
19
|
+
}
|
|
2
20
|
export async function runProcess(command, args, options) {
|
|
3
21
|
options.signal.throwIfAborted();
|
|
4
22
|
const signal = AbortSignal.any([
|
package/dist/tools/standard.js
CHANGED
|
@@ -4,13 +4,24 @@ import { dirname, relative } from "node:path";
|
|
|
4
4
|
import { textResult, } from "@alisio/sdk";
|
|
5
5
|
import { fileSize, readHead, readText } from "../runtime/fs.js";
|
|
6
6
|
import { safePath } from "../runtime/paths.js";
|
|
7
|
-
import { runProcess } from "../runtime/process.js";
|
|
7
|
+
import { isMissingCommand, RIPGREP_INSTALL_HINT, runProcess } from "../runtime/process.js";
|
|
8
8
|
import { EXECUTE_TIMEOUT_MS, MAX_NESTED_CALLS, runExecute } from "./execute.js";
|
|
9
9
|
import { searchWithFallback } from "./search.js";
|
|
10
10
|
import { DEFAULT_TIMEOUT_SECONDS, MAX_RESPONSE_BYTES, MAX_TIMEOUT_SECONDS, webfetch, } from "./webfetch.js";
|
|
11
11
|
const str = { type: "string" }, integer = { type: "integer", minimum: 0 };
|
|
12
12
|
export const objectSchema = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
13
13
|
export const hash = (text) => createHash("sha256").update(text).digest("hex");
|
|
14
|
+
/** Runs ripgrep and turns a missing binary into an actionable install message. */
|
|
15
|
+
async function runRipgrep(args, options) {
|
|
16
|
+
try {
|
|
17
|
+
return await runProcess("rg", args, options);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (isMissingCommand(error))
|
|
21
|
+
throw new Error(RIPGREP_INSTALL_HINT);
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
14
25
|
async function existing(path) {
|
|
15
26
|
const size = await fileSize(path);
|
|
16
27
|
if (size === undefined)
|
|
@@ -120,7 +131,7 @@ execCtx, websearchCtx) {
|
|
|
120
131
|
paths: filePaths,
|
|
121
132
|
async execute(i, c) {
|
|
122
133
|
const path = await safePath(c.workspace, String(i.path ?? "."));
|
|
123
|
-
const result = await
|
|
134
|
+
const result = await runRipgrep(["--files", "--glob", "!.git", "--glob", "!node_modules", "--", path], { cwd: c.workspace, signal: c.signal, maxBytes: 200_000 });
|
|
124
135
|
if (result.exitCode > 1 && !result.truncated)
|
|
125
136
|
throw new Error(result.stderr);
|
|
126
137
|
const all = result.stdout
|
|
@@ -146,7 +157,7 @@ execCtx, websearchCtx) {
|
|
|
146
157
|
paths: filePaths,
|
|
147
158
|
async execute(i, c) {
|
|
148
159
|
const path = await safePath(c.workspace, String(i.path ?? "."));
|
|
149
|
-
const r = await
|
|
160
|
+
const r = await runRipgrep([
|
|
150
161
|
"--json",
|
|
151
162
|
"--max-count",
|
|
152
163
|
"100",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the @alisio/core package version at runtime so the MCP client metadata stays in sync
|
|
3
|
+
* with the published package. Resolves the manifest relative to the module (`src/version.ts` →
|
|
4
|
+
* `../package.json` in the source tree; `dist/version.js` → `../package.json` when installed).
|
|
5
|
+
* Standalone binaries inject ALISIO_PACKAGE_VERSION at build time (scripts/binary-build.ts).
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadVersion(fromHere: string): string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* Fallback used when neither the build-time injection nor the package manifest is readable.
|
|
6
|
+
* A development marker, never a publish literal: it cannot silently desync from the package.
|
|
7
|
+
*/
|
|
8
|
+
const FALLBACK = "dev";
|
|
9
|
+
/**
|
|
10
|
+
* Reads the @alisio/core package version at runtime so the MCP client metadata stays in sync
|
|
11
|
+
* with the published package. Resolves the manifest relative to the module (`src/version.ts` →
|
|
12
|
+
* `../package.json` in the source tree; `dist/version.js` → `../package.json` when installed).
|
|
13
|
+
* Standalone binaries inject ALISIO_PACKAGE_VERSION at build time (scripts/binary-build.ts).
|
|
14
|
+
*/
|
|
15
|
+
export function loadVersion(fromHere) {
|
|
16
|
+
if (process.env.ALISIO_PACKAGE_VERSION)
|
|
17
|
+
return process.env.ALISIO_PACKAGE_VERSION;
|
|
18
|
+
try {
|
|
19
|
+
const manifest = join(dirname(fileURLToPath(fromHere)), "..", "package.json");
|
|
20
|
+
const parsed = JSON.parse(readFileSync(manifest, "utf8"));
|
|
21
|
+
return typeof parsed.version === "string" && parsed.version ? parsed.version : FALLBACK;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return FALLBACK;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alisio/core",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.6",
|
|
4
4
|
"description": "Embeddable provider-agnostic Alisio agent core: tool loop, context compaction, local tools, MCP, plugin host and SQLite session store. Runs on Node >=22.16 and Bun.",
|
|
5
5
|
"author": "Gustavo Gutiérrez",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"node": ">=22.16"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@alisio/sdk": "0.1.0-alpha.
|
|
44
|
+
"@alisio/sdk": "0.1.0-alpha.5",
|
|
45
45
|
"@mixmark-io/domino": "2.2.0",
|
|
46
46
|
"@modelcontextprotocol/client": "2.1.0",
|
|
47
47
|
"ajv": "8.20.0",
|