@24klynx/plugins 0.1.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/dist/index.d.mts +353 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +299 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +28 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Plugin system core types.
|
|
4
|
+
*
|
|
5
|
+
* Every plugin ships a manifest.json with static metadata and an
|
|
6
|
+
* entry point. The loader reads the manifest first, validates it,
|
|
7
|
+
* then lazily imports the entry module.
|
|
8
|
+
*/
|
|
9
|
+
/** Static metadata declared in a plugin's manifest.json. */
|
|
10
|
+
interface PluginManifest {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
description: string;
|
|
14
|
+
author?: string;
|
|
15
|
+
/** Relative path to the entry module, e.g. "./index.js". */
|
|
16
|
+
entry: string;
|
|
17
|
+
/** Lynx‑specific constraints and capabilities. */
|
|
18
|
+
lynx: {
|
|
19
|
+
minVersion: string; /** Which contract types this plugin fulfills. */
|
|
20
|
+
contracts: string[]; /** Declared capabilities. */
|
|
21
|
+
capabilities: {
|
|
22
|
+
tools?: string[];
|
|
23
|
+
hooks?: string[];
|
|
24
|
+
channels?: string[];
|
|
25
|
+
models?: string[];
|
|
26
|
+
}; /** Feature flags that must be enabled for this plugin. */
|
|
27
|
+
dangerousFlags?: string[]; /** Required permissions. */
|
|
28
|
+
permissions?: {
|
|
29
|
+
filesystem?: string;
|
|
30
|
+
network?: string;
|
|
31
|
+
exec?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The live plugin instance returned by the loader. */
|
|
36
|
+
interface PluginContract {
|
|
37
|
+
/** Same id as in the manifest. */
|
|
38
|
+
id: string;
|
|
39
|
+
manifest: PluginManifest;
|
|
40
|
+
/** Tools registered by this plugin. */
|
|
41
|
+
tools: PluginTool[];
|
|
42
|
+
/** Hooks registered by this plugin. */
|
|
43
|
+
hooks: PluginHook[];
|
|
44
|
+
/** Cleanup function called on unload. */
|
|
45
|
+
onUnload?: () => void | Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
/** A tool contributed by a plugin. */
|
|
48
|
+
interface PluginTool {
|
|
49
|
+
name: string;
|
|
50
|
+
description: string;
|
|
51
|
+
inputSchema: Record<string, unknown>;
|
|
52
|
+
handler: (input: Record<string, unknown>) => Promise<{
|
|
53
|
+
success: boolean;
|
|
54
|
+
content: string;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
/** A hook contributed by a plugin. */
|
|
58
|
+
interface PluginHook {
|
|
59
|
+
type: string;
|
|
60
|
+
/** Priority — lower numbers run first. */
|
|
61
|
+
priority: number;
|
|
62
|
+
handler: (event: unknown) => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Runtime context passed to every plugin during initialisation.
|
|
66
|
+
*
|
|
67
|
+
* This is the INTERNAL contract between the plugin loader and plugin modules.
|
|
68
|
+
* Plugin authors use the SDK's `PluginContext` (from `@lynx/plugin-sdk`),
|
|
69
|
+
* which wraps this with additional registration helpers.
|
|
70
|
+
*/
|
|
71
|
+
interface PluginRuntimeContext {
|
|
72
|
+
/** Unique plugin id assigned by the loader. */
|
|
73
|
+
pluginId: string;
|
|
74
|
+
/** Scoped logger for this plugin. */
|
|
75
|
+
logger: {
|
|
76
|
+
info(msg: string): void;
|
|
77
|
+
warn(msg: string): void;
|
|
78
|
+
error(msg: string): void;
|
|
79
|
+
};
|
|
80
|
+
/** Per‑plugin configuration from settings. */
|
|
81
|
+
config: Record<string, unknown>;
|
|
82
|
+
/** Per‑plugin persistent key‑value store. */
|
|
83
|
+
storage: {
|
|
84
|
+
get<T>(key: string): T | undefined;
|
|
85
|
+
set<T>(key: string, value: T): void;
|
|
86
|
+
delete(key: string): void;
|
|
87
|
+
clear(): void;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** The default export of a plugin entry module. */
|
|
91
|
+
type PluginEntryFunction = (ctx: PluginRuntimeContext) => PluginContract | Promise<PluginContract>;
|
|
92
|
+
/** All hook event names that plugins can subscribe to. */
|
|
93
|
+
type HookType = "before_tool_call" | "after_tool_call" | "before_llm_call" | "after_llm_call" | "on_session_start" | "on_session_end" | "on_message_received" | "on_message_sent" | "on_permission_decision";
|
|
94
|
+
interface HookSink {
|
|
95
|
+
type: HookType;
|
|
96
|
+
/** The owning plugin id. */
|
|
97
|
+
pluginId: string;
|
|
98
|
+
priority: number;
|
|
99
|
+
handler: (event: unknown) => Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/discovery.d.ts
|
|
103
|
+
interface ManifestEntry {
|
|
104
|
+
/** Absolute path to the plugin directory. */
|
|
105
|
+
dir: string;
|
|
106
|
+
/** Parsed manifest content. */
|
|
107
|
+
manifest: PluginManifest;
|
|
108
|
+
}
|
|
109
|
+
interface ManifestRegistry {
|
|
110
|
+
/** Scan one or more directories for plugin manifests. */
|
|
111
|
+
scan(directories: string[]): ManifestEntry[];
|
|
112
|
+
/** Get a plugin by id (fast lookup). */
|
|
113
|
+
getById(id: string): ManifestEntry | undefined;
|
|
114
|
+
/** List all discovered plugins. */
|
|
115
|
+
listAll(): ManifestEntry[];
|
|
116
|
+
/** How many plugins are registered. */
|
|
117
|
+
count(): number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Create a manifest registry.
|
|
121
|
+
*
|
|
122
|
+
* Call `scan()` during Phase 2 startup to populate it,
|
|
123
|
+
* then use `getById()` / `listAll()` for fast lookups.
|
|
124
|
+
*/
|
|
125
|
+
declare function createManifestRegistry(): ManifestRegistry;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/loader.d.ts
|
|
128
|
+
interface PluginLoader {
|
|
129
|
+
/** Load a plugin from its manifest entry. */
|
|
130
|
+
load(entry: ManifestEntry, ctx: PluginRuntimeContext): Promise<PluginContract>;
|
|
131
|
+
/** Unload a previously loaded plugin. */
|
|
132
|
+
unload(pluginName: string): void;
|
|
133
|
+
/** Get a loaded plugin. */
|
|
134
|
+
getLoaded(pluginName: string): PluginContract | undefined;
|
|
135
|
+
/** List all loaded plugin names. */
|
|
136
|
+
listLoaded(): string[];
|
|
137
|
+
/** Destroy all loaded plugins. */
|
|
138
|
+
destroy(): Promise<void>;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Create a plugin loader with lazy import and caching.
|
|
142
|
+
*/
|
|
143
|
+
declare function createPluginLoader(): PluginLoader;
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region src/hooks.d.ts
|
|
146
|
+
type FailureStrategy = "fail-open" | "fail-closed";
|
|
147
|
+
interface HookRegistry {
|
|
148
|
+
/** Register a sink. Lower priority runs first. */
|
|
149
|
+
register(sink: HookSink): void;
|
|
150
|
+
/** Remove all sinks for a plugin. */
|
|
151
|
+
unregisterPlugin(pluginId: string): void;
|
|
152
|
+
/** Dispatch an event to all registered hooks of this type. */
|
|
153
|
+
dispatch(type: HookType, event: unknown, strategy?: FailureStrategy): Promise<void>;
|
|
154
|
+
/** How many sinks are registered. */
|
|
155
|
+
count(): number;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create a hook registry.
|
|
159
|
+
*
|
|
160
|
+
* Hooks are stored in priority‑ordered arrays per hook type.
|
|
161
|
+
* Dispatch iterates them in order and short‑circuits on fail‑closed.
|
|
162
|
+
*/
|
|
163
|
+
declare function createHookRegistry(): HookRegistry;
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/predictive-loader.d.ts
|
|
166
|
+
/**
|
|
167
|
+
* Predictive loader — pre‑warms plugins before they are needed.
|
|
168
|
+
*
|
|
169
|
+
* Priority‑based warmup queue that loads plugins during idle time
|
|
170
|
+
* (Phase 2 startup background tasks) and reactively based on input
|
|
171
|
+
* prefix heuristics.
|
|
172
|
+
*
|
|
173
|
+
* Reference: §17 PredictiveLoader design
|
|
174
|
+
*/
|
|
175
|
+
/** A plugin module after lazy import(). */
|
|
176
|
+
type PluginModule = unknown;
|
|
177
|
+
declare class PredictiveLoader {
|
|
178
|
+
/** Map of already loaded plugins. */
|
|
179
|
+
private _loaded;
|
|
180
|
+
/** Set of plugin ids currently being imported. */
|
|
181
|
+
private _warming;
|
|
182
|
+
/** Priority queue (higher number = more important). */
|
|
183
|
+
private _queue;
|
|
184
|
+
/** Abort controller for cancelling in‑flight warmups. */
|
|
185
|
+
private _abort;
|
|
186
|
+
/** Dynamic import function (injectable for testing). */
|
|
187
|
+
private _importFn;
|
|
188
|
+
constructor(importFn: (id: string) => Promise<PluginModule>);
|
|
189
|
+
/** Whether the plugin has already been loaded. */
|
|
190
|
+
isLoaded(pluginId: string): boolean;
|
|
191
|
+
/** Whether the plugin is currently being warmed. */
|
|
192
|
+
isWarming(pluginId: string): boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Enqueue a plugin for warming at the given priority.
|
|
195
|
+
* Higher priority plugins are processed first.
|
|
196
|
+
*/
|
|
197
|
+
enqueue(pluginId: string, priority: number): void;
|
|
198
|
+
/** Process the entire warmup queue (non‑blocking — yields between imports). */
|
|
199
|
+
processQueue(): Promise<void>;
|
|
200
|
+
/** Stop all in‑flight warmups and clear the queue. */
|
|
201
|
+
abort(): void;
|
|
202
|
+
/** Number of plugins currently queued for warming. */
|
|
203
|
+
get pendingCount(): number;
|
|
204
|
+
/** Number of plugins that have been successfully loaded. */
|
|
205
|
+
get loadedCount(): number;
|
|
206
|
+
/**
|
|
207
|
+
* Predict which plugins to warm based on input prefix.
|
|
208
|
+
*
|
|
209
|
+
* Heuristics (200 ms window after prefix character):
|
|
210
|
+
* "/" → skill plugins
|
|
211
|
+
* "@" → model/provider plugins
|
|
212
|
+
*/
|
|
213
|
+
onInputPrefix(prefix: string): void;
|
|
214
|
+
/**
|
|
215
|
+
* Predict which plugins to warm based on a pending action.
|
|
216
|
+
* "file_read" → file operations plugin
|
|
217
|
+
* "code_edit" → editor tool plugin
|
|
218
|
+
*/
|
|
219
|
+
onActionPending(action: string): void;
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
//#region src/sdk/types.d.ts
|
|
223
|
+
/**
|
|
224
|
+
* Plugin SDK types — the context object passed to every plugin's
|
|
225
|
+
* register callback.
|
|
226
|
+
*/
|
|
227
|
+
/** Simple scoped logger for plugins. */
|
|
228
|
+
interface PluginLogger {
|
|
229
|
+
info(msg: string): void;
|
|
230
|
+
warn(msg: string): void;
|
|
231
|
+
error(msg: string): void;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The API surface available to a plugin during registration.
|
|
235
|
+
*
|
|
236
|
+
* Pattern: the plugin's `register(ctx)` callback receives this
|
|
237
|
+
* object and uses it to declare tools, hooks, and interact with
|
|
238
|
+
* the Lynx runtime.
|
|
239
|
+
*/
|
|
240
|
+
interface PluginContext {
|
|
241
|
+
/** Unique id assigned to this plugin instance. */
|
|
242
|
+
readonly pluginId: string;
|
|
243
|
+
/** Logger bound to this plugin's namespace. */
|
|
244
|
+
readonly logger: PluginLogger;
|
|
245
|
+
/** Plugin‑scoped key‑value store (persisted to disk). */
|
|
246
|
+
readonly storage: PluginStorage;
|
|
247
|
+
/** Emit an event that other plugins can subscribe to. */
|
|
248
|
+
emit(event: string, payload: unknown): void;
|
|
249
|
+
}
|
|
250
|
+
/** Simple key‑value store scoped to a single plugin. */
|
|
251
|
+
interface PluginStorage {
|
|
252
|
+
get<T = unknown>(key: string): T | undefined;
|
|
253
|
+
set<T = unknown>(key: string, value: T): void;
|
|
254
|
+
delete(key: string): void;
|
|
255
|
+
clear(): void;
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/sdk/define-plugin.d.ts
|
|
259
|
+
/** What a plugin's register function returns. */
|
|
260
|
+
interface PluginDefinition {
|
|
261
|
+
/** Display name. */
|
|
262
|
+
name: string;
|
|
263
|
+
/** Semver version string. */
|
|
264
|
+
version?: string;
|
|
265
|
+
/** Human‑readable description. */
|
|
266
|
+
description?: string;
|
|
267
|
+
/** Hook declarations (executed by the hook engine). */
|
|
268
|
+
hooks?: HookDeclaration[];
|
|
269
|
+
}
|
|
270
|
+
/** A hook declaration — static metadata, handler runs at runtime. */
|
|
271
|
+
interface HookDeclaration {
|
|
272
|
+
/** Hook event name (e.g. "before_agent_reply"). */
|
|
273
|
+
type: string;
|
|
274
|
+
/** "fail-open": hook failure doesn't block; "fail-closed": hook failure rejects. */
|
|
275
|
+
policy: "fail-open" | "fail-closed";
|
|
276
|
+
/** The handler function. */
|
|
277
|
+
handler: (event: unknown) => Promise<void>;
|
|
278
|
+
}
|
|
279
|
+
/** The register callback signature. */
|
|
280
|
+
type PluginRegisterFn = (ctx: PluginContext) => PluginDefinition | Promise<PluginDefinition>;
|
|
281
|
+
/** The shape returned by `definePlugin`. */
|
|
282
|
+
interface DefinedPlugin {
|
|
283
|
+
/** Unique plugin id (from manifest.json). */
|
|
284
|
+
id: string;
|
|
285
|
+
manifest: PluginManifest;
|
|
286
|
+
register: PluginRegisterFn;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Declare a Lynx plugin.
|
|
290
|
+
*
|
|
291
|
+
* `register` is called once at startup with a {@link PluginContext}
|
|
292
|
+
* that provides logging, storage, and event emission.
|
|
293
|
+
*/
|
|
294
|
+
declare function definePlugin(manifest: PluginManifest, register: PluginRegisterFn): DefinedPlugin;
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/sdk/define-tool.d.ts
|
|
297
|
+
/**
|
|
298
|
+
* `defineTool` — declare a tool that a plugin contributes to the agent.
|
|
299
|
+
*
|
|
300
|
+
* Pattern (inspired by FengMing's `api.registerTool`):
|
|
301
|
+
* const myTool = defineTool({
|
|
302
|
+
* name: "my_tool",
|
|
303
|
+
* description: "Does something useful",
|
|
304
|
+
* inputSchema: { type: "object", properties: {...} },
|
|
305
|
+
* handler: async (input) => ({ success: true, content: "done" }),
|
|
306
|
+
* });
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* Lightweight tool descriptor — the subset of internal `ToolDescriptor`
|
|
310
|
+
* that plugin authors need to provide. Extra fields (kind, safety, executor,
|
|
311
|
+
* owner) are filled in by the registration layer.
|
|
312
|
+
*/
|
|
313
|
+
interface SdkToolDescriptor {
|
|
314
|
+
name: string;
|
|
315
|
+
description: string;
|
|
316
|
+
inputSchema: Record<string, unknown>;
|
|
317
|
+
}
|
|
318
|
+
/** The shape a plugin author provides to define a tool. */
|
|
319
|
+
interface ToolBlueprint {
|
|
320
|
+
/** Unique tool name (e.g. "web_search"). */
|
|
321
|
+
name: string;
|
|
322
|
+
/** Human‑readable description for the LLM. */
|
|
323
|
+
description: string;
|
|
324
|
+
/** JSON Schema for the input parameters. */
|
|
325
|
+
inputSchema: Record<string, unknown>;
|
|
326
|
+
/** The tool implementation. */
|
|
327
|
+
handler: (input: Record<string, unknown>) => Promise<{
|
|
328
|
+
success: boolean;
|
|
329
|
+
content: string;
|
|
330
|
+
}>;
|
|
331
|
+
}
|
|
332
|
+
/** A defined tool — ready for registration. */
|
|
333
|
+
interface DefinedTool {
|
|
334
|
+
/** Unique tool name. */
|
|
335
|
+
name: string;
|
|
336
|
+
/** The tool descriptor that the registration layer consumes. */
|
|
337
|
+
descriptor: SdkToolDescriptor;
|
|
338
|
+
/** The tool implementation. */
|
|
339
|
+
handler: (input: Record<string, unknown>) => Promise<{
|
|
340
|
+
success: boolean;
|
|
341
|
+
content: string;
|
|
342
|
+
}>;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Declare a tool that can be registered by a plugin.
|
|
346
|
+
*
|
|
347
|
+
* The returned `DefinedTool` is passed to the plugin's register
|
|
348
|
+
* function or returned from `definePlugin`.
|
|
349
|
+
*/
|
|
350
|
+
declare function defineTool(blueprint: ToolBlueprint): DefinedTool;
|
|
351
|
+
//#endregion
|
|
352
|
+
export { type DefinedPlugin, type DefinedTool, type FailureStrategy, type HookDeclaration, type HookRegistry, type HookSink, type HookType, type ManifestEntry, type ManifestRegistry, type PluginContext, type PluginContract, type PluginDefinition, type PluginEntryFunction, type PluginHook, type PluginLoader, type PluginLogger, type PluginManifest, type PluginRegisterFn, type PluginRuntimeContext, type PluginStorage, type PluginTool, PredictiveLoader, type SdkToolDescriptor, type ToolBlueprint, createHookRegistry, createManifestRegistry, createPluginLoader, definePlugin, defineTool };
|
|
353
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/discovery.ts","../src/loader.ts","../src/hooks.ts","../src/predictive-loader.ts","../src/sdk/types.ts","../src/sdk/define-plugin.ts","../src/sdk/define-tool.ts"],"mappings":"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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { PluginError, PluginLoadError } from "@lynx/core";
|
|
4
|
+
//#region src/discovery.ts
|
|
5
|
+
/**
|
|
6
|
+
* Plugin discovery — scan directories for manifest.json files.
|
|
7
|
+
*
|
|
8
|
+
* The discovery layer only reads manifest metadata; it never imports
|
|
9
|
+
* plugin code. This keeps the registry fast and allows filtering
|
|
10
|
+
* before loading.
|
|
11
|
+
*/
|
|
12
|
+
const MANIFEST_FILE = "manifest.json";
|
|
13
|
+
/**
|
|
14
|
+
* Create a manifest registry.
|
|
15
|
+
*
|
|
16
|
+
* Call `scan()` during Phase 2 startup to populate it,
|
|
17
|
+
* then use `getById()` / `listAll()` for fast lookups.
|
|
18
|
+
*/
|
|
19
|
+
function createManifestRegistry() {
|
|
20
|
+
const entries = /* @__PURE__ */ new Map();
|
|
21
|
+
return {
|
|
22
|
+
scan(directories) {
|
|
23
|
+
const discovered = [];
|
|
24
|
+
for (const dir of directories) {
|
|
25
|
+
const manifestPath = join(dir, MANIFEST_FILE);
|
|
26
|
+
if (!existsSync(manifestPath)) continue;
|
|
27
|
+
let raw;
|
|
28
|
+
try {
|
|
29
|
+
raw = readFileSync(manifestPath, "utf-8");
|
|
30
|
+
} catch (err) {
|
|
31
|
+
throw new PluginLoadError$1(`Cannot read manifest at ${manifestPath}: ${String(err)}`);
|
|
32
|
+
}
|
|
33
|
+
let manifest;
|
|
34
|
+
try {
|
|
35
|
+
manifest = JSON.parse(raw);
|
|
36
|
+
} catch {
|
|
37
|
+
throw new PluginError(`Invalid JSON in manifest: ${manifestPath}`, {
|
|
38
|
+
category: "plugin",
|
|
39
|
+
userVisible: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!manifest.name || !manifest.version || !manifest.entry) throw new PluginError(`manifest.json at ${dir} is missing required fields (name, version, entry)`, {
|
|
43
|
+
category: "plugin",
|
|
44
|
+
userVisible: true
|
|
45
|
+
});
|
|
46
|
+
const entry = {
|
|
47
|
+
dir,
|
|
48
|
+
manifest
|
|
49
|
+
};
|
|
50
|
+
entries.set(manifest.name, entry);
|
|
51
|
+
discovered.push(entry);
|
|
52
|
+
}
|
|
53
|
+
return discovered;
|
|
54
|
+
},
|
|
55
|
+
getById(id) {
|
|
56
|
+
return entries.get(id);
|
|
57
|
+
},
|
|
58
|
+
listAll() {
|
|
59
|
+
return [...entries.values()];
|
|
60
|
+
},
|
|
61
|
+
count() {
|
|
62
|
+
return entries.size;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
var PluginLoadError$1 = class extends PluginError {
|
|
67
|
+
constructor(message) {
|
|
68
|
+
super(message, { userVisible: true });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/loader.ts
|
|
73
|
+
/**
|
|
74
|
+
* Create a plugin loader with lazy import and caching.
|
|
75
|
+
*/
|
|
76
|
+
function createPluginLoader() {
|
|
77
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
78
|
+
return {
|
|
79
|
+
async load(entry, ctx) {
|
|
80
|
+
const existing = loaded.get(entry.manifest.name);
|
|
81
|
+
if (existing) return existing;
|
|
82
|
+
let mod;
|
|
83
|
+
try {
|
|
84
|
+
mod = await import(`${entry.dir}/${entry.manifest.entry}`);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
throw new PluginLoadError(`Failed to import plugin "${entry.manifest.name}" from ${entry.manifest.entry}: ${String(err)}`);
|
|
87
|
+
}
|
|
88
|
+
if (!mod.default || typeof mod.default !== "function") throw new PluginError(`Plugin "${entry.manifest.name}" does not have a default export function`, {
|
|
89
|
+
category: "plugin",
|
|
90
|
+
userVisible: true
|
|
91
|
+
});
|
|
92
|
+
let contract;
|
|
93
|
+
try {
|
|
94
|
+
contract = await mod.default(ctx);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
throw new PluginError(`Plugin "${entry.manifest.name}" initialisation threw: ${String(err)}`, {
|
|
97
|
+
category: "plugin",
|
|
98
|
+
userVisible: true
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
loaded.set(entry.manifest.name, contract);
|
|
102
|
+
return contract;
|
|
103
|
+
},
|
|
104
|
+
unload(pluginName) {
|
|
105
|
+
const contract = loaded.get(pluginName);
|
|
106
|
+
if (contract?.onUnload) contract.onUnload();
|
|
107
|
+
loaded.delete(pluginName);
|
|
108
|
+
},
|
|
109
|
+
getLoaded(pluginName) {
|
|
110
|
+
return loaded.get(pluginName);
|
|
111
|
+
},
|
|
112
|
+
listLoaded() {
|
|
113
|
+
return [...loaded.keys()];
|
|
114
|
+
},
|
|
115
|
+
async destroy() {
|
|
116
|
+
for (const [, contract] of loaded) if (contract.onUnload) try {
|
|
117
|
+
await contract.onUnload();
|
|
118
|
+
} catch {}
|
|
119
|
+
loaded.clear();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/hooks.ts
|
|
125
|
+
/**
|
|
126
|
+
* Create a hook registry.
|
|
127
|
+
*
|
|
128
|
+
* Hooks are stored in priority‑ordered arrays per hook type.
|
|
129
|
+
* Dispatch iterates them in order and short‑circuits on fail‑closed.
|
|
130
|
+
*/
|
|
131
|
+
function createHookRegistry() {
|
|
132
|
+
const sinks = /* @__PURE__ */ new Map();
|
|
133
|
+
function ensureList(type) {
|
|
134
|
+
let list = sinks.get(type);
|
|
135
|
+
if (!list) {
|
|
136
|
+
list = [];
|
|
137
|
+
sinks.set(type, list);
|
|
138
|
+
}
|
|
139
|
+
return list;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
register(sink) {
|
|
143
|
+
const list = ensureList(sink.type);
|
|
144
|
+
list.push(sink);
|
|
145
|
+
list.sort((a, b) => a.priority - b.priority);
|
|
146
|
+
},
|
|
147
|
+
unregisterPlugin(pluginId) {
|
|
148
|
+
for (const [type, list] of sinks) {
|
|
149
|
+
const filtered = list.filter((s) => s.pluginId !== pluginId);
|
|
150
|
+
if (filtered.length === 0) sinks.delete(type);
|
|
151
|
+
else sinks.set(type, filtered);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
async dispatch(type, event, strategy = "fail-open") {
|
|
155
|
+
const list = sinks.get(type);
|
|
156
|
+
if (!list || list.length === 0) return;
|
|
157
|
+
for (const sink of list) try {
|
|
158
|
+
await sink.handler(event);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (strategy === "fail-closed") throw err;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
count() {
|
|
164
|
+
let total = 0;
|
|
165
|
+
for (const list of sinks.values()) total += list.length;
|
|
166
|
+
return total;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/predictive-loader.ts
|
|
172
|
+
var PredictiveLoader = class {
|
|
173
|
+
/** Map of already loaded plugins. */
|
|
174
|
+
_loaded = /* @__PURE__ */ new Map();
|
|
175
|
+
/** Set of plugin ids currently being imported. */
|
|
176
|
+
_warming = /* @__PURE__ */ new Set();
|
|
177
|
+
/** Priority queue (higher number = more important). */
|
|
178
|
+
_queue = [];
|
|
179
|
+
/** Abort controller for cancelling in‑flight warmups. */
|
|
180
|
+
_abort = new AbortController();
|
|
181
|
+
/** Dynamic import function (injectable for testing). */
|
|
182
|
+
_importFn;
|
|
183
|
+
constructor(importFn) {
|
|
184
|
+
this._importFn = importFn;
|
|
185
|
+
}
|
|
186
|
+
/** Whether the plugin has already been loaded. */
|
|
187
|
+
isLoaded(pluginId) {
|
|
188
|
+
return this._loaded.has(pluginId);
|
|
189
|
+
}
|
|
190
|
+
/** Whether the plugin is currently being warmed. */
|
|
191
|
+
isWarming(pluginId) {
|
|
192
|
+
return this._warming.has(pluginId);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Enqueue a plugin for warming at the given priority.
|
|
196
|
+
* Higher priority plugins are processed first.
|
|
197
|
+
*/
|
|
198
|
+
enqueue(pluginId, priority) {
|
|
199
|
+
if (this.isLoaded(pluginId) || this.isWarming(pluginId)) return;
|
|
200
|
+
const idx = this._queue.findIndex((e) => e.priority < priority);
|
|
201
|
+
if (idx < 0) this._queue.push({
|
|
202
|
+
pluginId,
|
|
203
|
+
priority
|
|
204
|
+
});
|
|
205
|
+
else this._queue.splice(idx, 0, {
|
|
206
|
+
pluginId,
|
|
207
|
+
priority
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/** Process the entire warmup queue (non‑blocking — yields between imports). */
|
|
211
|
+
async processQueue() {
|
|
212
|
+
while (this._queue.length > 0) {
|
|
213
|
+
if (this._abort.signal.aborted) return;
|
|
214
|
+
const entry = this._queue.shift();
|
|
215
|
+
if (this.isLoaded(entry.pluginId)) continue;
|
|
216
|
+
this._warming.add(entry.pluginId);
|
|
217
|
+
try {
|
|
218
|
+
const mod = await this._importFn(entry.pluginId);
|
|
219
|
+
this._loaded.set(entry.pluginId, mod);
|
|
220
|
+
} catch {} finally {
|
|
221
|
+
this._warming.delete(entry.pluginId);
|
|
222
|
+
}
|
|
223
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Stop all in‑flight warmups and clear the queue. */
|
|
227
|
+
abort() {
|
|
228
|
+
this._abort.abort();
|
|
229
|
+
this._queue.length = 0;
|
|
230
|
+
this._warming.clear();
|
|
231
|
+
}
|
|
232
|
+
/** Number of plugins currently queued for warming. */
|
|
233
|
+
get pendingCount() {
|
|
234
|
+
return this._queue.length;
|
|
235
|
+
}
|
|
236
|
+
/** Number of plugins that have been successfully loaded. */
|
|
237
|
+
get loadedCount() {
|
|
238
|
+
return this._loaded.size;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Predict which plugins to warm based on input prefix.
|
|
242
|
+
*
|
|
243
|
+
* Heuristics (200 ms window after prefix character):
|
|
244
|
+
* "/" → skill plugins
|
|
245
|
+
* "@" → model/provider plugins
|
|
246
|
+
*/
|
|
247
|
+
onInputPrefix(prefix) {
|
|
248
|
+
if (prefix === "/") this.enqueue("@lynx/skills", 100);
|
|
249
|
+
else if (prefix === "@") this.enqueue("@lynx/model-picker", 90);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Predict which plugins to warm based on a pending action.
|
|
253
|
+
* "file_read" → file operations plugin
|
|
254
|
+
* "code_edit" → editor tool plugin
|
|
255
|
+
*/
|
|
256
|
+
onActionPending(action) {
|
|
257
|
+
if (action.startsWith("file_")) this.enqueue("@lynx/tool-file-ops", 80);
|
|
258
|
+
else if (action === "code_edit") this.enqueue("@lynx/tool-edit", 80);
|
|
259
|
+
else if (action === "web_search") this.enqueue("@lynx/tool-web", 70);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/sdk/define-plugin.ts
|
|
264
|
+
/**
|
|
265
|
+
* Declare a Lynx plugin.
|
|
266
|
+
*
|
|
267
|
+
* `register` is called once at startup with a {@link PluginContext}
|
|
268
|
+
* that provides logging, storage, and event emission.
|
|
269
|
+
*/
|
|
270
|
+
function definePlugin(manifest, register) {
|
|
271
|
+
return {
|
|
272
|
+
id: manifest.name,
|
|
273
|
+
manifest,
|
|
274
|
+
register
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/sdk/define-tool.ts
|
|
279
|
+
/**
|
|
280
|
+
* Declare a tool that can be registered by a plugin.
|
|
281
|
+
*
|
|
282
|
+
* The returned `DefinedTool` is passed to the plugin's register
|
|
283
|
+
* function or returned from `definePlugin`.
|
|
284
|
+
*/
|
|
285
|
+
function defineTool(blueprint) {
|
|
286
|
+
return {
|
|
287
|
+
name: blueprint.name,
|
|
288
|
+
descriptor: {
|
|
289
|
+
name: blueprint.name,
|
|
290
|
+
description: blueprint.description,
|
|
291
|
+
inputSchema: blueprint.inputSchema
|
|
292
|
+
},
|
|
293
|
+
handler: blueprint.handler
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
export { PredictiveLoader, createHookRegistry, createManifestRegistry, createPluginLoader, definePlugin, defineTool };
|
|
298
|
+
|
|
299
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["PluginLoadError"],"sources":["../src/discovery.ts","../src/loader.ts","../src/hooks.ts","../src/predictive-loader.ts","../src/sdk/define-plugin.ts","../src/sdk/define-tool.ts"],"sourcesContent":["/**\n * Plugin discovery — scan directories for manifest.json files.\n *\n * The discovery layer only reads manifest metadata; it never imports\n * plugin code. This keeps the registry fast and allows filtering\n * before loading.\n */\n\nimport { readFileSync, existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { PluginManifest } from \"./types.js\";\nimport { PluginError } from \"@lynx/core\";\n\n// ── Types ──────────────────────────────────────────\n\nexport interface ManifestEntry {\n /** Absolute path to the plugin directory. */\n dir: string;\n /** Parsed manifest content. */\n manifest: PluginManifest;\n}\n\nexport interface ManifestRegistry {\n /** Scan one or more directories for plugin manifests. */\n scan(directories: string[]): ManifestEntry[];\n /** Get a plugin by id (fast lookup). */\n getById(id: string): ManifestEntry | undefined;\n /** List all discovered plugins. */\n listAll(): ManifestEntry[];\n /** How many plugins are registered. */\n count(): number;\n}\n\n// ── Constants ──────────────────────────────────────\n\nconst MANIFEST_FILE = \"manifest.json\";\n\n// ── Public API ─────────────────────────────────────\n\n/**\n * Create a manifest registry.\n *\n * Call `scan()` during Phase 2 startup to populate it,\n * then use `getById()` / `listAll()` for fast lookups.\n */\nexport function createManifestRegistry(): ManifestRegistry {\n const entries = new Map<string, ManifestEntry>();\n\n const registry: ManifestRegistry = {\n scan(directories: string[]): ManifestEntry[] {\n const discovered: ManifestEntry[] = [];\n\n for (const dir of directories) {\n const manifestPath = join(dir, MANIFEST_FILE);\n\n if (!existsSync(manifestPath)) continue;\n\n let raw: string;\n try {\n raw = readFileSync(manifestPath, \"utf-8\");\n } catch (err) {\n throw new PluginLoadError(`Cannot read manifest at ${manifestPath}: ${String(err)}`);\n }\n\n let manifest: PluginManifest;\n try {\n manifest = JSON.parse(raw) as PluginManifest;\n } catch {\n throw new PluginError(`Invalid JSON in manifest: ${manifestPath}`, {\n category: \"plugin\",\n userVisible: true,\n });\n }\n\n // Validate required fields\n if (!manifest.name || !manifest.version || !manifest.entry) {\n throw new PluginError(\n `manifest.json at ${dir} is missing required fields (name, version, entry)`,\n { category: \"plugin\", userVisible: true },\n );\n }\n\n const entry: ManifestEntry = { dir, manifest };\n entries.set(manifest.name, entry);\n discovered.push(entry);\n }\n\n return discovered;\n },\n\n getById(id: string): ManifestEntry | undefined {\n return entries.get(id);\n },\n\n listAll(): ManifestEntry[] {\n return [...entries.values()];\n },\n\n count(): number {\n return entries.size;\n },\n };\n\n return registry;\n}\n\n// ── Error helper ───────────────────────────────────\n\nclass PluginLoadError extends PluginError {\n constructor(message: string) {\n super(message, { userVisible: true });\n }\n}\n","/**\n * Plugin loader — lazy import() with caching.\n *\n * Plugins are loaded once and cached. If a plugin fails to load,\n * the error is isolated — other plugins continue working.\n *\n * Fail‑open: a plugin that throws during init is unloaded but\n * the app continues without it.\n * Fail‑closed: for security‑critical hooks, a failing plugin\n * blocks the operation (see hooks.ts).\n */\n\nimport type { PluginContract, PluginRuntimeContext, PluginEntryFunction } from \"./types.js\";\nimport type { ManifestEntry } from \"./discovery.js\";\nimport { PluginError, PluginLoadError } from \"@lynx/core\";\n\n// ── Types ──────────────────────────────────────────\n\nexport interface PluginLoader {\n /** Load a plugin from its manifest entry. */\n load(entry: ManifestEntry, ctx: PluginRuntimeContext): Promise<PluginContract>;\n /** Unload a previously loaded plugin. */\n unload(pluginName: string): void;\n /** Get a loaded plugin. */\n getLoaded(pluginName: string): PluginContract | undefined;\n /** List all loaded plugin names. */\n listLoaded(): string[];\n /** Destroy all loaded plugins. */\n destroy(): Promise<void>;\n}\n\n// ── Public API ─────────────────────────────────────\n\n/**\n * Create a plugin loader with lazy import and caching.\n */\nexport function createPluginLoader(): PluginLoader {\n const loaded = new Map<string, PluginContract>();\n\n const loader: PluginLoader = {\n async load(entry: ManifestEntry, ctx: PluginRuntimeContext): Promise<PluginContract> {\n // Return cached if already loaded\n const existing = loaded.get(entry.manifest.name);\n if (existing) return existing;\n\n let mod: { default?: PluginEntryFunction };\n try {\n // Dynamic import — the entry path is relative to the plugin directory\n const entryPath = `${entry.dir}/${entry.manifest.entry}`;\n mod = await import(entryPath);\n } catch (err) {\n throw new PluginLoadError(\n `Failed to import plugin \"${entry.manifest.name}\" from ${entry.manifest.entry}: ${String(err)}`,\n );\n }\n\n if (!mod.default || typeof mod.default !== \"function\") {\n throw new PluginError(\n `Plugin \"${entry.manifest.name}\" does not have a default export function`,\n { category: \"plugin\", userVisible: true },\n );\n }\n\n let contract: PluginContract;\n try {\n contract = await mod.default(ctx);\n } catch (err) {\n throw new PluginError(\n `Plugin \"${entry.manifest.name}\" initialisation threw: ${String(err)}`,\n { category: \"plugin\", userVisible: true },\n );\n }\n\n loaded.set(entry.manifest.name, contract);\n return contract;\n },\n\n unload(pluginName: string): void {\n const contract = loaded.get(pluginName);\n if (contract?.onUnload) {\n void contract.onUnload();\n }\n loaded.delete(pluginName);\n },\n\n getLoaded(pluginName: string): PluginContract | undefined {\n return loaded.get(pluginName);\n },\n\n listLoaded(): string[] {\n return [...loaded.keys()];\n },\n\n async destroy(): Promise<void> {\n for (const [, contract] of loaded) {\n if (contract.onUnload) {\n try {\n await contract.onUnload();\n } catch {\n // Best effort cleanup\n }\n }\n }\n loaded.clear();\n },\n };\n\n return loader;\n}\n","/**\n * Hook engine — dispatch events to registered plugin hooks.\n *\n * Supports two failure strategies:\n * fail‑open — hook failure does not block the operation (default)\n * fail‑closed — hook failure blocks the operation (security‑critical)\n */\n\nimport type { HookType, HookSink } from \"./types.js\";\n\n// ── Types ──────────────────────────────────────────\n\nexport type FailureStrategy = \"fail-open\" | \"fail-closed\";\n\nexport interface HookRegistry {\n /** Register a sink. Lower priority runs first. */\n register(sink: HookSink): void;\n /** Remove all sinks for a plugin. */\n unregisterPlugin(pluginId: string): void;\n /** Dispatch an event to all registered hooks of this type. */\n dispatch(type: HookType, event: unknown, strategy?: FailureStrategy): Promise<void>;\n /** How many sinks are registered. */\n count(): number;\n}\n\n// ── Public API ─────────────────────────────────────\n\n/**\n * Create a hook registry.\n *\n * Hooks are stored in priority‑ordered arrays per hook type.\n * Dispatch iterates them in order and short‑circuits on fail‑closed.\n */\nexport function createHookRegistry(): HookRegistry {\n const sinks = new Map<HookType, HookSink[]>();\n\n function ensureList(type: HookType): HookSink[] {\n let list = sinks.get(type);\n if (!list) {\n list = [];\n sinks.set(type, list);\n }\n return list;\n }\n\n const registry: HookRegistry = {\n register(sink: HookSink): void {\n const list = ensureList(sink.type);\n list.push(sink);\n // Keep sorted by priority (ascending)\n list.sort((a, b) => a.priority - b.priority);\n },\n\n unregisterPlugin(pluginId: string): void {\n for (const [type, list] of sinks) {\n const filtered = list.filter((s) => s.pluginId !== pluginId);\n if (filtered.length === 0) {\n sinks.delete(type);\n } else {\n sinks.set(type, filtered);\n }\n }\n },\n\n async dispatch(\n type: HookType,\n event: unknown,\n strategy: FailureStrategy = \"fail-open\",\n ): Promise<void> {\n const list = sinks.get(type);\n if (!list || list.length === 0) return;\n\n for (const sink of list) {\n try {\n await sink.handler(event);\n } catch (err) {\n if (strategy === \"fail-closed\") {\n throw err;\n }\n // fail‑open: continue to next hook\n }\n }\n },\n\n count(): number {\n let total = 0;\n for (const list of sinks.values()) {\n total += list.length;\n }\n return total;\n },\n };\n\n return registry;\n}\n","/**\n * Predictive loader — pre‑warms plugins before they are needed.\n *\n * Priority‑based warmup queue that loads plugins during idle time\n * (Phase 2 startup background tasks) and reactively based on input\n * prefix heuristics.\n *\n * Reference: §17 PredictiveLoader design\n */\n\n// ── Types ────────────────────────────────────────────\n\n/** A plugin module after lazy import(). */\ntype PluginModule = unknown;\n\ninterface QueueEntry {\n pluginId: string;\n priority: number;\n}\n\n// ── PredictiveLoader ─────────────────────────────────\n\nexport class PredictiveLoader {\n /** Map of already loaded plugins. */\n private _loaded = new Map<string, PluginModule>();\n\n /** Set of plugin ids currently being imported. */\n private _warming = new Set<string>();\n\n /** Priority queue (higher number = more important). */\n private _queue: QueueEntry[] = [];\n\n /** Abort controller for cancelling in‑flight warmups. */\n private _abort = new AbortController();\n\n /** Dynamic import function (injectable for testing). */\n private _importFn: (id: string) => Promise<PluginModule>;\n\n constructor(importFn: (id: string) => Promise<PluginModule>) {\n this._importFn = importFn;\n }\n\n // ── Public API ─────────────────────────────────────\n\n /** Whether the plugin has already been loaded. */\n isLoaded(pluginId: string): boolean {\n return this._loaded.has(pluginId);\n }\n\n /** Whether the plugin is currently being warmed. */\n isWarming(pluginId: string): boolean {\n return this._warming.has(pluginId);\n }\n\n /**\n * Enqueue a plugin for warming at the given priority.\n * Higher priority plugins are processed first.\n */\n enqueue(pluginId: string, priority: number): void {\n if (this.isLoaded(pluginId) || this.isWarming(pluginId)) {\n return; // Already done or in progress — idempotent\n }\n\n // Insert sorted by priority (descending)\n const idx = this._queue.findIndex((e) => e.priority < priority);\n if (idx < 0) {\n this._queue.push({ pluginId, priority });\n } else {\n this._queue.splice(idx, 0, { pluginId, priority });\n }\n }\n\n /** Process the entire warmup queue (non‑blocking — yields between imports). */\n async processQueue(): Promise<void> {\n while (this._queue.length > 0) {\n if (this._abort.signal.aborted) return;\n\n const entry = this._queue.shift()!;\n if (this.isLoaded(entry.pluginId)) continue;\n\n this._warming.add(entry.pluginId);\n\n try {\n const mod = await this._importFn(entry.pluginId);\n this._loaded.set(entry.pluginId, mod);\n } catch {\n // Silently skip plugins that fail to warm — they'll be\n // retried (with an error report) when actually requested.\n } finally {\n this._warming.delete(entry.pluginId);\n }\n\n // Yield to the event loop so the main thread stays responsive.\n await new Promise((resolve) => setImmediate(resolve));\n }\n }\n\n /** Stop all in‑flight warmups and clear the queue. */\n abort(): void {\n this._abort.abort();\n this._queue.length = 0;\n this._warming.clear();\n }\n\n /** Number of plugins currently queued for warming. */\n get pendingCount(): number {\n return this._queue.length;\n }\n\n /** Number of plugins that have been successfully loaded. */\n get loadedCount(): number {\n return this._loaded.size;\n }\n\n // ── Input prediction ───────────────────────────────\n\n /**\n * Predict which plugins to warm based on input prefix.\n *\n * Heuristics (200 ms window after prefix character):\n * \"/\" → skill plugins\n * \"@\" → model/provider plugins\n */\n onInputPrefix(prefix: string): void {\n if (prefix === \"/\") {\n this.enqueue(\"@lynx/skills\", 100);\n } else if (prefix === \"@\") {\n this.enqueue(\"@lynx/model-picker\", 90);\n }\n }\n\n /**\n * Predict which plugins to warm based on a pending action.\n * \"file_read\" → file operations plugin\n * \"code_edit\" → editor tool plugin\n */\n onActionPending(action: string): void {\n if (action.startsWith(\"file_\")) {\n this.enqueue(\"@lynx/tool-file-ops\", 80);\n } else if (action === \"code_edit\") {\n this.enqueue(\"@lynx/tool-edit\", 80);\n } else if (action === \"web_search\") {\n this.enqueue(\"@lynx/tool-web\", 70);\n }\n }\n}\n","/**\n * `definePlugin` — the entry point for every Lynx plugin.\n *\n * Pattern (inspired by FengMing's `definePluginEntry`):\n * export default definePlugin((ctx) => {\n * ctx.logger.info(\"loaded\");\n * return { name: \"my-plugin\", tools: [...] };\n * });\n */\n\nimport type { PluginContext } from \"./types.js\";\nimport type { PluginManifest } from \"../types.js\";\n\n// ── Types ────────────────────────────────────────────\n\n/** What a plugin's register function returns. */\nexport interface PluginDefinition {\n /** Display name. */\n name: string;\n /** Semver version string. */\n version?: string;\n /** Human‑readable description. */\n description?: string;\n /** Hook declarations (executed by the hook engine). */\n hooks?: HookDeclaration[];\n}\n\n/** A hook declaration — static metadata, handler runs at runtime. */\nexport interface HookDeclaration {\n /** Hook event name (e.g. \"before_agent_reply\"). */\n type: string;\n /** \"fail-open\": hook failure doesn't block; \"fail-closed\": hook failure rejects. */\n policy: \"fail-open\" | \"fail-closed\";\n /** The handler function. */\n handler: (event: unknown) => Promise<void>;\n}\n\n/** The register callback signature. */\nexport type PluginRegisterFn = (ctx: PluginContext) => PluginDefinition | Promise<PluginDefinition>;\n\n/** The shape returned by `definePlugin`. */\nexport interface DefinedPlugin {\n /** Unique plugin id (from manifest.json). */\n id: string;\n manifest: PluginManifest;\n register: PluginRegisterFn;\n}\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Declare a Lynx plugin.\n *\n * `register` is called once at startup with a {@link PluginContext}\n * that provides logging, storage, and event emission.\n */\nexport function definePlugin(manifest: PluginManifest, register: PluginRegisterFn): DefinedPlugin {\n return {\n id: manifest.name,\n manifest,\n register,\n };\n}\n","/**\n * `defineTool` — declare a tool that a plugin contributes to the agent.\n *\n * Pattern (inspired by FengMing's `api.registerTool`):\n * const myTool = defineTool({\n * name: \"my_tool\",\n * description: \"Does something useful\",\n * inputSchema: { type: \"object\", properties: {...} },\n * handler: async (input) => ({ success: true, content: \"done\" }),\n * });\n */\n\n// ── Types ────────────────────────────────────────────\n\n/**\n * Lightweight tool descriptor — the subset of internal `ToolDescriptor`\n * that plugin authors need to provide. Extra fields (kind, safety, executor,\n * owner) are filled in by the registration layer.\n */\nexport interface SdkToolDescriptor {\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n}\n\n/** The shape a plugin author provides to define a tool. */\nexport interface ToolBlueprint {\n /** Unique tool name (e.g. \"web_search\"). */\n name: string;\n /** Human‑readable description for the LLM. */\n description: string;\n /** JSON Schema for the input parameters. */\n inputSchema: Record<string, unknown>;\n /** The tool implementation. */\n handler: (input: Record<string, unknown>) => Promise<{ success: boolean; content: string }>;\n}\n\n/** A defined tool — ready for registration. */\nexport interface DefinedTool {\n /** Unique tool name. */\n name: string;\n /** The tool descriptor that the registration layer consumes. */\n descriptor: SdkToolDescriptor;\n /** The tool implementation. */\n handler: (input: Record<string, unknown>) => Promise<{ success: boolean; content: string }>;\n}\n\n// ── Public API ───────────────────────────────────────\n\n/**\n * Declare a tool that can be registered by a plugin.\n *\n * The returned `DefinedTool` is passed to the plugin's register\n * function or returned from `definePlugin`.\n */\nexport function defineTool(blueprint: ToolBlueprint): DefinedTool {\n return {\n name: blueprint.name,\n descriptor: {\n name: blueprint.name,\n description: blueprint.description,\n inputSchema: blueprint.inputSchema,\n },\n handler: blueprint.handler,\n };\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@24klynx/plugins",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Plugin system — discovery, loading, hook engine, SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.mts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@24klynx/core": "0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsdown --config-loader tsx",
|
|
25
|
+
"test": "vitest run --passWithNoTests",
|
|
26
|
+
"typecheck": "tsgo --noEmit"
|
|
27
|
+
}
|
|
28
|
+
}
|