@nexrall/code-core 1.4.67 → 1.4.69
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/agent/agentRegistry.d.ts +27 -0
- package/dist/agent/agentRegistry.d.ts.map +1 -1
- package/dist/agent/agentRegistry.js +45 -0
- package/dist/agent/hooks.d.ts +7 -0
- package/dist/agent/hooks.d.ts.map +1 -1
- package/dist/agent/hooks.js +14 -3
- package/dist/agent/loop.d.ts +28 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +93 -10
- package/dist/agent/subTask.d.ts +10 -1
- package/dist/agent/subTask.d.ts.map +1 -1
- package/dist/agent/subTask.js +23 -6
- package/dist/agent/subTaskSupport.d.ts +11 -0
- package/dist/agent/subTaskSupport.d.ts.map +1 -1
- package/dist/agent/subTaskSupport.js +17 -1
- package/dist/api/client.d.ts +67 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +180 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/mcp/server.d.ts +39 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +189 -0
- package/dist/plugins/data.d.ts +23 -0
- package/dist/plugins/data.d.ts.map +1 -0
- package/dist/plugins/data.js +141 -0
- package/dist/plugins/index.d.ts +14 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +45 -4
- package/dist/plugins/installer.d.ts +83 -0
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +207 -1
- package/package.json +1 -1
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parsePluginDependencies = parsePluginDependencies;
|
|
36
37
|
exports.searchRegistry = searchRegistry;
|
|
37
38
|
exports.getRegistryPlugin = getRegistryPlugin;
|
|
38
39
|
exports.reportInstall = reportInstall;
|
|
@@ -44,14 +45,46 @@ exports.installPlugin = installPlugin;
|
|
|
44
45
|
exports.readReceipt = readReceipt;
|
|
45
46
|
exports.removePlugin = removePlugin;
|
|
46
47
|
exports.updatePlugin = updatePlugin;
|
|
48
|
+
exports.planPluginPrune = planPluginPrune;
|
|
49
|
+
exports.prunePlugins = prunePlugins;
|
|
47
50
|
const fs = __importStar(require("fs"));
|
|
48
51
|
const path = __importStar(require("path"));
|
|
49
52
|
const os = __importStar(require("os"));
|
|
50
53
|
const child_process_1 = require("child_process");
|
|
51
54
|
const client_1 = require("../api/client");
|
|
52
55
|
const index_1 = require("./index");
|
|
56
|
+
const data_1 = require("./data");
|
|
53
57
|
const sources_1 = require("./sources");
|
|
54
58
|
const frontmatter_1 = require("../util/frontmatter");
|
|
59
|
+
/** Read a manifest's `dependencies` list (either spelling), dropping malformed entries. */
|
|
60
|
+
function parsePluginDependencies(dir) {
|
|
61
|
+
const file = (0, index_1.resolvePluginFile)(dir, 'manifest');
|
|
62
|
+
if (!file)
|
|
63
|
+
return [];
|
|
64
|
+
try {
|
|
65
|
+
const j = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
66
|
+
if (!Array.isArray(j.dependencies))
|
|
67
|
+
return [];
|
|
68
|
+
const out = [];
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
for (const raw of j.dependencies) {
|
|
71
|
+
const obj = raw && typeof raw === 'object' ? raw : null;
|
|
72
|
+
const name = typeof raw === 'string' ? raw : typeof obj?.name === 'string' ? obj.name : '';
|
|
73
|
+
const trimmed = name.trim();
|
|
74
|
+
// Same rule as an install name: a dependency can never escape the plugins
|
|
75
|
+
// directory, and a repeat entry adds nothing.
|
|
76
|
+
if (!/^[\w.-]+$/.test(trimmed) || seen.has(trimmed))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(trimmed);
|
|
79
|
+
const version = typeof obj?.version === 'string' ? obj.version : undefined;
|
|
80
|
+
out.push(version ? { name: trimmed, version } : { name: trimmed });
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return []; // unreadable/malformed manifest — an install will report the real problem
|
|
86
|
+
}
|
|
87
|
+
}
|
|
55
88
|
const RECEIPT_FILE = '_install.json';
|
|
56
89
|
/** Search the registry (empty query = list all, official first). */
|
|
57
90
|
async function searchRegistry(query, limit = 50) {
|
|
@@ -170,6 +203,7 @@ function inspectPluginDir(dir) {
|
|
|
170
203
|
hasHooks: (0, index_1.resolvePluginFile)(dir, 'hooks') !== null,
|
|
171
204
|
hasMcp: (0, index_1.resolvePluginFile)(dir, 'mcp') !== null,
|
|
172
205
|
meta,
|
|
206
|
+
dependencies: parsePluginDependencies(dir),
|
|
173
207
|
};
|
|
174
208
|
}
|
|
175
209
|
/** Read a flat `commands/*.md` or `agents/*.md` directory, returning name + frontmatter description. */
|
|
@@ -428,6 +462,42 @@ function pluginsRoot(scope, workDir) {
|
|
|
428
462
|
? path.join(workDir || process.cwd(), '.nexrall', 'plugins')
|
|
429
463
|
: path.join(os.homedir(), '.nexrall', 'plugins');
|
|
430
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Directory of an installed plugin of this name, from EITHER scope (project first,
|
|
467
|
+
* matching loader precedence), or null. Used to decide that a dependency is already
|
|
468
|
+
* satisfied: the loader serves both scopes into one session, so an install in either
|
|
469
|
+
* one is a plugin the plugin can actually use.
|
|
470
|
+
*/
|
|
471
|
+
function installedPluginDir(name, workDir) {
|
|
472
|
+
if (!/^[\w.-]+$/.test(name))
|
|
473
|
+
return null;
|
|
474
|
+
for (const scope of ['project', 'global']) {
|
|
475
|
+
const dir = path.join(pluginsRoot(scope, workDir), name);
|
|
476
|
+
if (fs.existsSync(dir))
|
|
477
|
+
return dir;
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Where would installing dependency `name` come from? A declared source alias wins
|
|
483
|
+
* (the more specific, locally-declared intent), then the registry — the same order
|
|
484
|
+
* `nex plugin install <name>` uses, so the two can never disagree about what a name
|
|
485
|
+
* means.
|
|
486
|
+
*/
|
|
487
|
+
async function resolveDependencySpec(name, workDir) {
|
|
488
|
+
const alias = (0, sources_1.resolveSourceAlias)(name, workDir ?? process.cwd(), { includeProject: true });
|
|
489
|
+
if (alias)
|
|
490
|
+
return { spec: alias.spec };
|
|
491
|
+
try {
|
|
492
|
+
const entry = await getRegistryPlugin(name);
|
|
493
|
+
if (entry)
|
|
494
|
+
return { spec: entry.source };
|
|
495
|
+
}
|
|
496
|
+
catch (err) {
|
|
497
|
+
return { error: `the registry could not be reached (${err.message})` };
|
|
498
|
+
}
|
|
499
|
+
return { error: 'it is not a declared source and not in the registry' };
|
|
500
|
+
}
|
|
431
501
|
async function installPlugin(spec, opts = {}) {
|
|
432
502
|
const source = parsePluginSource(spec);
|
|
433
503
|
const scope = opts.scope ?? 'global';
|
|
@@ -496,6 +566,7 @@ async function installPlugin(spec, opts = {}) {
|
|
|
496
566
|
sha: sha ?? undefined,
|
|
497
567
|
previousSha: opts._previousSha,
|
|
498
568
|
isUpdate: Boolean(opts._previousSha) || Boolean(opts.force),
|
|
569
|
+
...(opts._auto ? { requiredBy: opts._auto.requiredBy } : {}),
|
|
499
570
|
});
|
|
500
571
|
if (!ok)
|
|
501
572
|
throw new Error('Installation cancelled.');
|
|
@@ -509,6 +580,52 @@ async function installPlugin(spec, opts = {}) {
|
|
|
509
580
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
510
581
|
}
|
|
511
582
|
copyPluginDir(pluginRoot, dest);
|
|
583
|
+
// 5b. Dependencies — install the ones that are missing, at the same scope.
|
|
584
|
+
// A plugin that calls another plugin's MCP server or skill is broken unless its
|
|
585
|
+
// dependency is present, and "install A, then go read A's manifest to find out
|
|
586
|
+
// what else to install" is exactly the step users skip. The safety gate still
|
|
587
|
+
// runs for each dependency: a dependency can ship hooks of its own.
|
|
588
|
+
const manifestDeps = parsePluginDependencies(pluginRoot).filter((d) => d.name !== name);
|
|
589
|
+
const dependencyWarnings = [];
|
|
590
|
+
const installedDependencies = [];
|
|
591
|
+
// Every directory this call wrote, newest last. A failed install removes them
|
|
592
|
+
// again — the point of a failure is that nothing changed, and a half-installed
|
|
593
|
+
// plugin directory is worse than no plugin at all: it LOADS.
|
|
594
|
+
const writtenDirs = [dest];
|
|
595
|
+
try {
|
|
596
|
+
for (const dep of manifestDeps) {
|
|
597
|
+
if (dep.version) {
|
|
598
|
+
dependencyWarnings.push(`"${dep.name}" declares version "${dep.version}", which is not enforced yet (sources are git specs, not versioned registry entries) — its default branch is used.`);
|
|
599
|
+
}
|
|
600
|
+
if ((opts._auto?.stack ?? []).includes(dep.name))
|
|
601
|
+
continue; // cycle: A → B → A
|
|
602
|
+
if (installedPluginDir(dep.name, opts.workDir))
|
|
603
|
+
continue; // satisfied by either scope
|
|
604
|
+
const resolved = await resolveDependencySpec(dep.name, opts.workDir);
|
|
605
|
+
if ('error' in resolved) {
|
|
606
|
+
throw new Error(`Dependency "${dep.name}" of "${name}": ${resolved.error}. Install it first: nex plugin install ${dep.name}`);
|
|
607
|
+
}
|
|
608
|
+
const child = await installPlugin(resolved.spec, {
|
|
609
|
+
scope,
|
|
610
|
+
workDir: opts.workDir,
|
|
611
|
+
name: dep.name,
|
|
612
|
+
confirm: opts.confirm,
|
|
613
|
+
_auto: { requiredBy: name, stack: [...(opts._auto?.stack ?? []), name] },
|
|
614
|
+
});
|
|
615
|
+
writtenDirs.push(...(child.writtenDirs ?? [child.dir]));
|
|
616
|
+
installedDependencies.push(dep.name, ...(child.installedDependencies ?? []));
|
|
617
|
+
dependencyWarnings.push(...(child.dependencyWarnings ?? []));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
for (const d of writtenDirs.reverse()) {
|
|
622
|
+
try {
|
|
623
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
624
|
+
}
|
|
625
|
+
catch { /* best effort */ }
|
|
626
|
+
}
|
|
627
|
+
throw err;
|
|
628
|
+
}
|
|
512
629
|
// 6. Receipt for `nex plugin update`
|
|
513
630
|
const receipt = {
|
|
514
631
|
source: source.raw,
|
|
@@ -517,9 +634,21 @@ async function installPlugin(spec, opts = {}) {
|
|
|
517
634
|
subdir: source.subdir,
|
|
518
635
|
installedAt: new Date().toISOString(),
|
|
519
636
|
...(sha ? { sha } : {}),
|
|
637
|
+
...(opts._auto ? { auto: true, requiredBy: opts._auto.requiredBy } : {}),
|
|
638
|
+
...(manifestDeps.length ? { dependencies: manifestDeps.map((d) => d.name) } : {}),
|
|
520
639
|
};
|
|
521
640
|
fs.writeFileSync(path.join(dest, RECEIPT_FILE), JSON.stringify(receipt, null, 2));
|
|
522
|
-
return {
|
|
641
|
+
return {
|
|
642
|
+
name,
|
|
643
|
+
dir: dest,
|
|
644
|
+
scope,
|
|
645
|
+
inspection,
|
|
646
|
+
sha: sha ?? undefined,
|
|
647
|
+
previousSha: opts._previousSha,
|
|
648
|
+
...(installedDependencies.length ? { installedDependencies } : {}),
|
|
649
|
+
...(dependencyWarnings.length ? { dependencyWarnings } : {}),
|
|
650
|
+
writtenDirs,
|
|
651
|
+
};
|
|
523
652
|
}
|
|
524
653
|
finally {
|
|
525
654
|
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
@@ -543,6 +672,11 @@ function removePlugin(name, opts = {}) {
|
|
|
543
672
|
const dir = path.join(pluginsRoot(s, opts.workDir), name);
|
|
544
673
|
if (fs.existsSync(dir)) {
|
|
545
674
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
675
|
+
// Data goes with the plugin unless --keep-data: the install directory is gone,
|
|
676
|
+
// and leaving a database/cache behind with no owner is how a later reinstall
|
|
677
|
+
// silently inherits state nobody meant to keep.
|
|
678
|
+
if (!opts.keepData)
|
|
679
|
+
(0, data_1.removePluginData)(name);
|
|
546
680
|
return dir;
|
|
547
681
|
}
|
|
548
682
|
}
|
|
@@ -576,4 +710,76 @@ async function updatePlugin(name, opts = {}) {
|
|
|
576
710
|
}
|
|
577
711
|
throw new Error(`Plugin "${name}" is not installed.`);
|
|
578
712
|
}
|
|
713
|
+
/** What would `nex plugin prune` remove? Pure inspection — installs/removes nothing. */
|
|
714
|
+
function planPluginPrune(opts = {}) {
|
|
715
|
+
const scopes = opts.scope ? [opts.scope] : ['project', 'global'];
|
|
716
|
+
const rows = [];
|
|
717
|
+
for (const s of scopes) {
|
|
718
|
+
let entries;
|
|
719
|
+
try {
|
|
720
|
+
entries = fs.readdirSync(pluginsRoot(s, opts.workDir), { withFileTypes: true });
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
continue; // scope has no plugins directory — nothing to prune there
|
|
724
|
+
}
|
|
725
|
+
for (const e of entries) {
|
|
726
|
+
if (!e.isDirectory())
|
|
727
|
+
continue;
|
|
728
|
+
const dir = path.join(pluginsRoot(s, opts.workDir), e.name);
|
|
729
|
+
rows.push({ name: e.name, scope: s, dir, receipt: readReceipt(dir) });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const byName = new Map();
|
|
733
|
+
for (const r of rows)
|
|
734
|
+
byName.set(r.name, [...(byName.get(r.name) ?? []), r]);
|
|
735
|
+
const needed = new Set();
|
|
736
|
+
const stack = rows.filter((r) => !r.receipt?.auto).map((r) => r.name);
|
|
737
|
+
while (stack.length) {
|
|
738
|
+
const n = stack.pop();
|
|
739
|
+
if (needed.has(n))
|
|
740
|
+
continue;
|
|
741
|
+
needed.add(n);
|
|
742
|
+
for (const r of byName.get(n) ?? []) {
|
|
743
|
+
// Union of the receipt (what was installed) and the manifest as it stands now
|
|
744
|
+
// (a hand-edited manifest must not lead to pruning something it still names).
|
|
745
|
+
const deps = new Set([
|
|
746
|
+
...(r.receipt?.dependencies ?? []),
|
|
747
|
+
...parsePluginDependencies(r.dir).map((d) => d.name),
|
|
748
|
+
]);
|
|
749
|
+
for (const d of deps)
|
|
750
|
+
if (!needed.has(d))
|
|
751
|
+
stack.push(d);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
const orphans = [];
|
|
755
|
+
const kept = [];
|
|
756
|
+
for (const r of rows) {
|
|
757
|
+
if (r.receipt?.auto && !needed.has(r.name)) {
|
|
758
|
+
orphans.push({ name: r.name, scope: r.scope, dir: r.dir, requiredBy: r.receipt.requiredBy });
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
kept.push({ name: r.name, scope: r.scope, reason: r.receipt?.auto ? 'needed' : 'manual' });
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return { orphans, kept };
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Remove a plan's orphans. Returns what was actually removed — a directory that
|
|
768
|
+
* could not be deleted is left in the plan AND out of the result, so the caller
|
|
769
|
+
* reports reality rather than the intention.
|
|
770
|
+
*/
|
|
771
|
+
function prunePlugins(plan, opts = {}) {
|
|
772
|
+
const removed = [];
|
|
773
|
+
for (const o of plan.orphans) {
|
|
774
|
+
try {
|
|
775
|
+
fs.rmSync(o.dir, { recursive: true, force: true });
|
|
776
|
+
removed.push(o);
|
|
777
|
+
// Same rule as removePlugin: uninstalling takes the data unless told to keep it.
|
|
778
|
+
if (!opts.keepData)
|
|
779
|
+
(0, data_1.removePluginData)(o.name);
|
|
780
|
+
}
|
|
781
|
+
catch { /* locked directory, permissions — leave it and report the rest */ }
|
|
782
|
+
}
|
|
783
|
+
return removed;
|
|
784
|
+
}
|
|
579
785
|
//# sourceMappingURL=installer.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.69",
|
|
4
4
|
"description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
|