notoken-core 1.2.1 → 1.3.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/handlers/executor.js +12 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/plugins/index.d.ts +5 -0
- package/dist/plugins/index.js +4 -0
- package/dist/plugins/registry.d.ts +86 -0
- package/dist/plugins/registry.js +280 -0
- package/dist/plugins/types.d.ts +104 -0
- package/dist/plugins/types.js +8 -0
- package/dist/utils/config.js +23 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { detectLocalPlatform, getPackageForCommand, getInstallCommand } from "..
|
|
|
8
8
|
import { withSpinner } from "../utils/spinner.js";
|
|
9
9
|
import { analyzeOutput } from "../utils/analysis.js";
|
|
10
10
|
import { smartRead, smartSearch } from "../utils/smartFile.js";
|
|
11
|
+
import { pluginRegistry } from "../plugins/registry.js";
|
|
11
12
|
/**
|
|
12
13
|
* Generic command executor.
|
|
13
14
|
*
|
|
@@ -19,6 +20,15 @@ export async function executeIntent(intent) {
|
|
|
19
20
|
if (!def) {
|
|
20
21
|
throw new Error(`No intent definition found for: ${intent.intent}`);
|
|
21
22
|
}
|
|
23
|
+
// Plugin beforeExecute hooks — can cancel execution
|
|
24
|
+
const proceed = await pluginRegistry.runBeforeExecute({
|
|
25
|
+
intent: intent.intent,
|
|
26
|
+
fields: intent.fields,
|
|
27
|
+
rawText: intent.rawText,
|
|
28
|
+
});
|
|
29
|
+
if (proceed === false) {
|
|
30
|
+
return "[cancelled by plugin]";
|
|
31
|
+
}
|
|
22
32
|
// Fuzzy resolve file paths if needed
|
|
23
33
|
const resolved = await resolveFuzzyFields(intent);
|
|
24
34
|
const fields = resolved.fields;
|
|
@@ -121,6 +131,8 @@ export async function executeIntent(intent) {
|
|
|
121
131
|
if (analysis) {
|
|
122
132
|
result += "\n" + analysis;
|
|
123
133
|
}
|
|
134
|
+
// Plugin afterExecute hooks
|
|
135
|
+
await pluginRegistry.runAfterExecute({ intent: intent.intent, fields }, result);
|
|
124
136
|
return result;
|
|
125
137
|
}
|
|
126
138
|
async function executeGitIntent(intentName, fields) {
|
package/dist/index.d.ts
CHANGED
|
@@ -48,5 +48,7 @@ export { checkForUpdate, checkForUpdateSync, runUpdate, formatUpdateBanner, type
|
|
|
48
48
|
export { logFailure, loadFailures, clearFailures } from "./utils/logger.js";
|
|
49
49
|
export { logUncertainty, loadUncertaintyLog, getUncertaintySummary } from "./nlp/uncertainty.js";
|
|
50
50
|
export { recordHistory, loadHistory, getRecentHistory, searchHistory } from "./context/history.js";
|
|
51
|
+
export { pluginRegistry } from "./plugins/index.js";
|
|
52
|
+
export type { NotokenPlugin, PluginIntent, PluginPlaybook, PluginHooks, LoadedPlugin } from "./plugins/index.js";
|
|
51
53
|
export type { DynamicIntent, ParsedCommand, IntentDef, FieldDef, EnvironmentName } from "./types/intent.js";
|
|
52
54
|
export type { RulePatch, RulePatchChange, FailureLog, RulesConfig } from "./types/rules.js";
|
package/dist/index.js
CHANGED
|
@@ -62,3 +62,5 @@ export { checkForUpdate, checkForUpdateSync, runUpdate, formatUpdateBanner } fro
|
|
|
62
62
|
export { logFailure, loadFailures, clearFailures } from "./utils/logger.js";
|
|
63
63
|
export { logUncertainty, loadUncertaintyLog, getUncertaintySummary } from "./nlp/uncertainty.js";
|
|
64
64
|
export { recordHistory, loadHistory, getRecentHistory, searchHistory } from "./context/history.js";
|
|
65
|
+
// ── Plugins ──
|
|
66
|
+
export { pluginRegistry } from "./plugins/index.js";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin registry.
|
|
3
|
+
*
|
|
4
|
+
* Discovers, loads, validates, and manages plugins.
|
|
5
|
+
*
|
|
6
|
+
* Plugin sources (checked in order):
|
|
7
|
+
* 1. ~/.notoken/plugins/ — local plugins (JS/TS files or directories)
|
|
8
|
+
* 2. npm: notoken-plugin-* — installed globally via npm
|
|
9
|
+
* 3. Built-in plugins — shipped with notoken-core
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { pluginRegistry } from "notoken-core";
|
|
13
|
+
* await pluginRegistry.loadAll();
|
|
14
|
+
* const intents = pluginRegistry.getAllIntents();
|
|
15
|
+
* const playbooks = pluginRegistry.getAllPlaybooks();
|
|
16
|
+
*/
|
|
17
|
+
import type { LoadedPlugin } from "./types.js";
|
|
18
|
+
declare class PluginRegistry {
|
|
19
|
+
private plugins;
|
|
20
|
+
private loaded;
|
|
21
|
+
/**
|
|
22
|
+
* Load all plugins from all sources.
|
|
23
|
+
*/
|
|
24
|
+
loadAll(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Load a single plugin by path or module name.
|
|
27
|
+
*/
|
|
28
|
+
load(nameOrPath: string, source?: "npm" | "local"): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Unload a plugin by name.
|
|
31
|
+
*/
|
|
32
|
+
unload(name: string): Promise<boolean>;
|
|
33
|
+
/** Get all intents from all plugins + core. */
|
|
34
|
+
getAllIntents(): Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
[k: string]: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
/** Get all playbooks from all plugins. */
|
|
39
|
+
getAllPlaybooks(): Array<{
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
steps: Array<{
|
|
43
|
+
command: string;
|
|
44
|
+
label: string;
|
|
45
|
+
}>;
|
|
46
|
+
_plugin: string;
|
|
47
|
+
}>;
|
|
48
|
+
/** Get all service aliases from plugins. */
|
|
49
|
+
getAllServiceAliases(): Record<string, string[]>;
|
|
50
|
+
/** Get all environment aliases from plugins. */
|
|
51
|
+
getAllEnvironmentAliases(): Record<string, string[]>;
|
|
52
|
+
/** Run all beforeExecute hooks. Returns false if any hook cancels. */
|
|
53
|
+
runBeforeExecute(intent: {
|
|
54
|
+
intent: string;
|
|
55
|
+
fields: Record<string, unknown>;
|
|
56
|
+
rawText: string;
|
|
57
|
+
}): Promise<boolean>;
|
|
58
|
+
/** Run all afterExecute hooks. */
|
|
59
|
+
runAfterExecute(intent: {
|
|
60
|
+
intent: string;
|
|
61
|
+
fields: Record<string, unknown>;
|
|
62
|
+
}, result: string): Promise<void>;
|
|
63
|
+
/** Run all onError hooks. */
|
|
64
|
+
runOnError(intent: {
|
|
65
|
+
intent: string;
|
|
66
|
+
fields: Record<string, unknown>;
|
|
67
|
+
}, error: Error): Promise<void>;
|
|
68
|
+
/** Run all onUnknown hooks. Returns first non-null result. */
|
|
69
|
+
runOnUnknown(rawText: string): Promise<{
|
|
70
|
+
intent: string;
|
|
71
|
+
fields: Record<string, unknown>;
|
|
72
|
+
confidence: number;
|
|
73
|
+
} | null>;
|
|
74
|
+
/** List all loaded plugins. */
|
|
75
|
+
list(): LoadedPlugin[];
|
|
76
|
+
/** Get a plugin by name. */
|
|
77
|
+
get(name: string): LoadedPlugin | undefined;
|
|
78
|
+
/** Get count of loaded plugins. */
|
|
79
|
+
get count(): number;
|
|
80
|
+
private loadLocalPlugins;
|
|
81
|
+
private loadNpmPlugins;
|
|
82
|
+
private validate;
|
|
83
|
+
}
|
|
84
|
+
/** Singleton plugin registry. */
|
|
85
|
+
export declare const pluginRegistry: PluginRegistry;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin registry.
|
|
3
|
+
*
|
|
4
|
+
* Discovers, loads, validates, and manages plugins.
|
|
5
|
+
*
|
|
6
|
+
* Plugin sources (checked in order):
|
|
7
|
+
* 1. ~/.notoken/plugins/ — local plugins (JS/TS files or directories)
|
|
8
|
+
* 2. npm: notoken-plugin-* — installed globally via npm
|
|
9
|
+
* 3. Built-in plugins — shipped with notoken-core
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { pluginRegistry } from "notoken-core";
|
|
13
|
+
* await pluginRegistry.loadAll();
|
|
14
|
+
* const intents = pluginRegistry.getAllIntents();
|
|
15
|
+
* const playbooks = pluginRegistry.getAllPlaybooks();
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
18
|
+
import { resolve, join } from "node:path";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
import { USER_HOME } from "../utils/paths.js";
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const PLUGIN_DIR = resolve(USER_HOME, "plugins");
|
|
24
|
+
class PluginRegistry {
|
|
25
|
+
plugins = new Map();
|
|
26
|
+
loaded = false;
|
|
27
|
+
/**
|
|
28
|
+
* Load all plugins from all sources.
|
|
29
|
+
*/
|
|
30
|
+
async loadAll() {
|
|
31
|
+
if (this.loaded)
|
|
32
|
+
return;
|
|
33
|
+
this.loaded = true;
|
|
34
|
+
// 1. Local plugins
|
|
35
|
+
await this.loadLocalPlugins();
|
|
36
|
+
// 2. npm global plugins
|
|
37
|
+
await this.loadNpmPlugins();
|
|
38
|
+
// Report
|
|
39
|
+
if (this.plugins.size > 0) {
|
|
40
|
+
const names = Array.from(this.plugins.keys());
|
|
41
|
+
console.error(`\x1b[2m[plugins] Loaded ${names.length}: ${names.join(", ")}\x1b[0m`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load a single plugin by path or module name.
|
|
46
|
+
*/
|
|
47
|
+
async load(nameOrPath, source = "local") {
|
|
48
|
+
try {
|
|
49
|
+
let plugin = null;
|
|
50
|
+
let pluginPath = nameOrPath;
|
|
51
|
+
// Try CJS require first (works for .js with module.exports), then ESM import
|
|
52
|
+
try {
|
|
53
|
+
const mod = require(nameOrPath);
|
|
54
|
+
plugin = (mod && typeof mod.name === "string") ? mod :
|
|
55
|
+
(mod?.default && typeof mod.default.name === "string") ? mod.default : null;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Fall back to ESM import
|
|
59
|
+
try {
|
|
60
|
+
const mod = await import(nameOrPath);
|
|
61
|
+
plugin = (mod.default && typeof mod.default.name === "string" ? mod.default :
|
|
62
|
+
typeof mod.name === "string" ? mod :
|
|
63
|
+
null);
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
}
|
|
67
|
+
if (!plugin || !plugin.name) {
|
|
68
|
+
console.error(`[plugins] Invalid plugin (no name): ${nameOrPath}`);
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
// Validate
|
|
72
|
+
if (!this.validate(plugin))
|
|
73
|
+
return false;
|
|
74
|
+
this.plugins.set(plugin.name, {
|
|
75
|
+
plugin: plugin,
|
|
76
|
+
source,
|
|
77
|
+
path: pluginPath,
|
|
78
|
+
enabled: true,
|
|
79
|
+
});
|
|
80
|
+
// Call onLoad hook
|
|
81
|
+
if (plugin.hooks?.onLoad) {
|
|
82
|
+
await plugin.hooks.onLoad();
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
console.error(`[plugins] Failed to load ${nameOrPath}: ${err.message}`);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Unload a plugin by name.
|
|
93
|
+
*/
|
|
94
|
+
async unload(name) {
|
|
95
|
+
const loaded = this.plugins.get(name);
|
|
96
|
+
if (!loaded)
|
|
97
|
+
return false;
|
|
98
|
+
if (loaded.plugin.hooks?.onUnload) {
|
|
99
|
+
await loaded.plugin.hooks.onUnload();
|
|
100
|
+
}
|
|
101
|
+
this.plugins.delete(name);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// ── Getters ──
|
|
105
|
+
/** Get all intents from all plugins + core. */
|
|
106
|
+
getAllIntents() {
|
|
107
|
+
const intents = [];
|
|
108
|
+
for (const { plugin, enabled } of this.plugins.values()) {
|
|
109
|
+
if (!enabled || !plugin.intents)
|
|
110
|
+
continue;
|
|
111
|
+
for (const intent of plugin.intents) {
|
|
112
|
+
intents.push({
|
|
113
|
+
...intent,
|
|
114
|
+
fields: intent.fields ?? {},
|
|
115
|
+
execution: intent.execution ?? "local",
|
|
116
|
+
requiresConfirmation: intent.requiresConfirmation ?? false,
|
|
117
|
+
riskLevel: intent.riskLevel ?? "low",
|
|
118
|
+
examples: intent.examples ?? [],
|
|
119
|
+
_plugin: plugin.name,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return intents;
|
|
124
|
+
}
|
|
125
|
+
/** Get all playbooks from all plugins. */
|
|
126
|
+
getAllPlaybooks() {
|
|
127
|
+
const playbooks = [];
|
|
128
|
+
for (const { plugin, enabled } of this.plugins.values()) {
|
|
129
|
+
if (!enabled || !plugin.playbooks)
|
|
130
|
+
continue;
|
|
131
|
+
for (const pb of plugin.playbooks) {
|
|
132
|
+
playbooks.push({ ...pb, _plugin: plugin.name });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return playbooks;
|
|
136
|
+
}
|
|
137
|
+
/** Get all service aliases from plugins. */
|
|
138
|
+
getAllServiceAliases() {
|
|
139
|
+
const aliases = {};
|
|
140
|
+
for (const { plugin, enabled } of this.plugins.values()) {
|
|
141
|
+
if (!enabled || !plugin.aliases?.services)
|
|
142
|
+
continue;
|
|
143
|
+
for (const [service, aliasList] of Object.entries(plugin.aliases.services)) {
|
|
144
|
+
aliases[service] = [...(aliases[service] ?? []), ...aliasList];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return aliases;
|
|
148
|
+
}
|
|
149
|
+
/** Get all environment aliases from plugins. */
|
|
150
|
+
getAllEnvironmentAliases() {
|
|
151
|
+
const aliases = {};
|
|
152
|
+
for (const { plugin, enabled } of this.plugins.values()) {
|
|
153
|
+
if (!enabled || !plugin.aliases?.environments)
|
|
154
|
+
continue;
|
|
155
|
+
for (const [env, aliasList] of Object.entries(plugin.aliases.environments)) {
|
|
156
|
+
aliases[env] = [...(aliases[env] ?? []), ...aliasList];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return aliases;
|
|
160
|
+
}
|
|
161
|
+
/** Run all beforeExecute hooks. Returns false if any hook cancels. */
|
|
162
|
+
async runBeforeExecute(intent) {
|
|
163
|
+
for (const { plugin, enabled } of this.plugins.values()) {
|
|
164
|
+
if (!enabled || !plugin.hooks?.beforeExecute)
|
|
165
|
+
continue;
|
|
166
|
+
const result = await plugin.hooks.beforeExecute(intent);
|
|
167
|
+
if (result === false)
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
/** Run all afterExecute hooks. */
|
|
173
|
+
async runAfterExecute(intent, result) {
|
|
174
|
+
for (const { plugin, enabled } of this.plugins.values()) {
|
|
175
|
+
if (!enabled || !plugin.hooks?.afterExecute)
|
|
176
|
+
continue;
|
|
177
|
+
try {
|
|
178
|
+
await plugin.hooks.afterExecute(intent, result);
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Run all onError hooks. */
|
|
184
|
+
async runOnError(intent, error) {
|
|
185
|
+
for (const { plugin, enabled } of this.plugins.values()) {
|
|
186
|
+
if (!enabled || !plugin.hooks?.onError)
|
|
187
|
+
continue;
|
|
188
|
+
try {
|
|
189
|
+
await plugin.hooks.onError(intent, error);
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Run all onUnknown hooks. Returns first non-null result. */
|
|
195
|
+
async runOnUnknown(rawText) {
|
|
196
|
+
for (const { plugin, enabled } of this.plugins.values()) {
|
|
197
|
+
if (!enabled || !plugin.hooks?.onUnknown)
|
|
198
|
+
continue;
|
|
199
|
+
try {
|
|
200
|
+
const result = await plugin.hooks.onUnknown(rawText);
|
|
201
|
+
if (result)
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
catch { }
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
/** List all loaded plugins. */
|
|
209
|
+
list() {
|
|
210
|
+
return Array.from(this.plugins.values());
|
|
211
|
+
}
|
|
212
|
+
/** Get a plugin by name. */
|
|
213
|
+
get(name) {
|
|
214
|
+
return this.plugins.get(name);
|
|
215
|
+
}
|
|
216
|
+
/** Get count of loaded plugins. */
|
|
217
|
+
get count() {
|
|
218
|
+
return this.plugins.size;
|
|
219
|
+
}
|
|
220
|
+
// ── Private ──
|
|
221
|
+
async loadLocalPlugins() {
|
|
222
|
+
if (!existsSync(PLUGIN_DIR))
|
|
223
|
+
return;
|
|
224
|
+
const entries = readdirSync(PLUGIN_DIR, { withFileTypes: true });
|
|
225
|
+
for (const entry of entries) {
|
|
226
|
+
const fullPath = resolve(PLUGIN_DIR, entry.name);
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
// Directory plugin — look for index.js or index.mjs
|
|
229
|
+
const indexPath = [
|
|
230
|
+
join(fullPath, "index.js"),
|
|
231
|
+
join(fullPath, "index.mjs"),
|
|
232
|
+
join(fullPath, "index.cjs"),
|
|
233
|
+
].find((p) => existsSync(p));
|
|
234
|
+
if (indexPath)
|
|
235
|
+
await this.load(indexPath, "local");
|
|
236
|
+
}
|
|
237
|
+
else if (entry.name.endsWith(".js") || entry.name.endsWith(".mjs")) {
|
|
238
|
+
await this.load(fullPath, "local");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async loadNpmPlugins() {
|
|
243
|
+
try {
|
|
244
|
+
const result = execSync("npm list -g --depth=0 --json 2>/dev/null", {
|
|
245
|
+
encoding: "utf-8",
|
|
246
|
+
timeout: 10_000,
|
|
247
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
248
|
+
});
|
|
249
|
+
const parsed = JSON.parse(result);
|
|
250
|
+
const deps = Object.keys(parsed.dependencies ?? {});
|
|
251
|
+
for (const dep of deps) {
|
|
252
|
+
if (dep.startsWith("notoken-plugin-")) {
|
|
253
|
+
await this.load(dep, "npm");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch { }
|
|
258
|
+
}
|
|
259
|
+
validate(plugin) {
|
|
260
|
+
if (!plugin.name || typeof plugin.name !== "string") {
|
|
261
|
+
console.error("[plugins] Plugin missing name");
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
if (plugin.name.includes(" ") || plugin.name.includes("/")) {
|
|
265
|
+
console.error(`[plugins] Invalid plugin name: "${plugin.name}" (no spaces or slashes)`);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
if (plugin.intents) {
|
|
269
|
+
for (const intent of plugin.intents) {
|
|
270
|
+
if (!intent.name || !intent.command) {
|
|
271
|
+
console.error(`[plugins] Plugin "${plugin.name}" has intent missing name or command`);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Singleton plugin registry. */
|
|
280
|
+
export const pluginRegistry = new PluginRegistry();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin type definitions.
|
|
3
|
+
*
|
|
4
|
+
* A plugin is a plain object that conforms to this interface.
|
|
5
|
+
* Minimum: { name, intents }
|
|
6
|
+
* Everything else is optional.
|
|
7
|
+
*/
|
|
8
|
+
export interface PluginIntent {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
synonyms: string[];
|
|
12
|
+
fields?: Record<string, {
|
|
13
|
+
type: string;
|
|
14
|
+
required: boolean;
|
|
15
|
+
default?: unknown;
|
|
16
|
+
}>;
|
|
17
|
+
command: string;
|
|
18
|
+
execution?: "local" | "remote";
|
|
19
|
+
requiresConfirmation?: boolean;
|
|
20
|
+
riskLevel?: "low" | "medium" | "high";
|
|
21
|
+
allowlist?: string[];
|
|
22
|
+
examples?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface PluginPlaybook {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
steps: Array<{
|
|
28
|
+
command: string;
|
|
29
|
+
label: string;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export interface PluginHooks {
|
|
33
|
+
/** Called before any intent executes. Return false to cancel. */
|
|
34
|
+
beforeExecute?: (intent: {
|
|
35
|
+
intent: string;
|
|
36
|
+
fields: Record<string, unknown>;
|
|
37
|
+
rawText: string;
|
|
38
|
+
}) => Promise<boolean | void>;
|
|
39
|
+
/** Called after any intent executes successfully. */
|
|
40
|
+
afterExecute?: (intent: {
|
|
41
|
+
intent: string;
|
|
42
|
+
fields: Record<string, unknown>;
|
|
43
|
+
}, result: string) => Promise<void>;
|
|
44
|
+
/** Called when an intent execution fails. */
|
|
45
|
+
onError?: (intent: {
|
|
46
|
+
intent: string;
|
|
47
|
+
fields: Record<string, unknown>;
|
|
48
|
+
}, error: Error) => Promise<void>;
|
|
49
|
+
/** Called when no intent matches. Return a result to handle it, or null to pass. */
|
|
50
|
+
onUnknown?: (rawText: string) => Promise<{
|
|
51
|
+
intent: string;
|
|
52
|
+
fields: Record<string, unknown>;
|
|
53
|
+
confidence: number;
|
|
54
|
+
} | null>;
|
|
55
|
+
/** Called on startup. */
|
|
56
|
+
onLoad?: () => Promise<void>;
|
|
57
|
+
/** Called on shutdown. */
|
|
58
|
+
onUnload?: () => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export interface PluginAliases {
|
|
61
|
+
services?: Record<string, string[]>;
|
|
62
|
+
environments?: Record<string, string[]>;
|
|
63
|
+
}
|
|
64
|
+
export interface PluginFileHints {
|
|
65
|
+
[category: string]: {
|
|
66
|
+
aliases: string[];
|
|
67
|
+
configs?: Array<{
|
|
68
|
+
path: string;
|
|
69
|
+
description: string;
|
|
70
|
+
}>;
|
|
71
|
+
logs?: Array<{
|
|
72
|
+
path: string;
|
|
73
|
+
description: string;
|
|
74
|
+
}>;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export interface NotokenPlugin {
|
|
78
|
+
/** Unique plugin name (e.g., "aws", "mycompany", "slack") */
|
|
79
|
+
name: string;
|
|
80
|
+
/** Plugin version */
|
|
81
|
+
version?: string;
|
|
82
|
+
/** Description */
|
|
83
|
+
description?: string;
|
|
84
|
+
/** Author */
|
|
85
|
+
author?: string;
|
|
86
|
+
/** New intents this plugin adds */
|
|
87
|
+
intents?: PluginIntent[];
|
|
88
|
+
/** New playbooks */
|
|
89
|
+
playbooks?: PluginPlaybook[];
|
|
90
|
+
/** Lifecycle hooks */
|
|
91
|
+
hooks?: PluginHooks;
|
|
92
|
+
/** New service/environment aliases */
|
|
93
|
+
aliases?: PluginAliases;
|
|
94
|
+
/** New file hint locations */
|
|
95
|
+
fileHints?: PluginFileHints;
|
|
96
|
+
/** Plugin configuration (user can set via notoken config set plugins.<name>.<key>) */
|
|
97
|
+
config?: Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
export interface LoadedPlugin {
|
|
100
|
+
plugin: NotokenPlugin;
|
|
101
|
+
source: "npm" | "local" | "builtin";
|
|
102
|
+
path: string;
|
|
103
|
+
enabled: boolean;
|
|
104
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import { RulesConfig } from "../types/rules.js";
|
|
4
4
|
import { IntentsConfig } from "../types/intent.js";
|
|
5
5
|
import { CONFIG_DIR } from "./paths.js";
|
|
6
|
+
import { pluginRegistry } from "../plugins/registry.js";
|
|
6
7
|
let cachedRules = null;
|
|
7
8
|
let cachedIntents = null;
|
|
8
9
|
export function loadRules(forceReload = false) {
|
|
@@ -10,6 +11,20 @@ export function loadRules(forceReload = false) {
|
|
|
10
11
|
return cachedRules;
|
|
11
12
|
const raw = readFileSync(resolve(CONFIG_DIR, "rules.json"), "utf-8");
|
|
12
13
|
cachedRules = RulesConfig.parse(JSON.parse(raw));
|
|
14
|
+
// Merge plugin aliases into rules
|
|
15
|
+
const pluginServices = pluginRegistry.getAllServiceAliases();
|
|
16
|
+
for (const [service, aliases] of Object.entries(pluginServices)) {
|
|
17
|
+
if (!cachedRules.serviceAliases[service]) {
|
|
18
|
+
cachedRules.serviceAliases[service] = aliases;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
for (const alias of aliases) {
|
|
22
|
+
if (!cachedRules.serviceAliases[service].includes(alias)) {
|
|
23
|
+
cachedRules.serviceAliases[service].push(alias);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
13
28
|
return cachedRules;
|
|
14
29
|
}
|
|
15
30
|
export function loadIntents(forceReload = false) {
|
|
@@ -17,6 +32,14 @@ export function loadIntents(forceReload = false) {
|
|
|
17
32
|
return cachedIntents.intents;
|
|
18
33
|
const raw = readFileSync(resolve(CONFIG_DIR, "intents.json"), "utf-8");
|
|
19
34
|
cachedIntents = IntentsConfig.parse(JSON.parse(raw));
|
|
35
|
+
// Merge plugin intents
|
|
36
|
+
const pluginIntents = pluginRegistry.getAllIntents();
|
|
37
|
+
for (const pi of pluginIntents) {
|
|
38
|
+
// Don't add duplicates
|
|
39
|
+
if (!cachedIntents.intents.find((i) => i.name === pi.name)) {
|
|
40
|
+
cachedIntents.intents.push(pi);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
20
43
|
return cachedIntents.intents;
|
|
21
44
|
}
|
|
22
45
|
export function getIntentDef(name) {
|