notoken-core 1.2.0 → 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/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # notoken-core
2
+
3
+ Shared engine for [notoken](https://notoken.sh) — NLP parsing, execution, detection, analysis.
4
+
5
+ Used by the CLI (`notoken`) and the desktop app (`notoken-installer`).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install notoken-core
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```typescript
16
+ import {
17
+ parseIntent,
18
+ executeIntent,
19
+ detectLocalPlatform,
20
+ checkForUpdate,
21
+ } from "notoken-core";
22
+
23
+ // Parse natural language into a structured intent
24
+ const parsed = await parseIntent("restart nginx on prod");
25
+ console.log(parsed.intent.intent); // "service.restart"
26
+ console.log(parsed.intent.fields); // { service: "nginx", environment: "prod" }
27
+
28
+ // Detect platform
29
+ const platform = detectLocalPlatform();
30
+ console.log(platform.distro); // "Ubuntu 24.04.2 LTS"
31
+ console.log(platform.packageManager); // "apt"
32
+ ```
33
+
34
+ ## What's Inside
35
+
36
+ - **119 config-driven intents** — services, docker, git, files, network, security, databases, and more
37
+ - **NLP pipeline** — rule parser + compromise POS tagging + multi-classifier + keyboard typo correction
38
+ - **LLM fallback** — Claude CLI, OpenAI API, or Ollama (auto-detected)
39
+ - **File parsers** — passwd, shadow, .env, yaml, json, nginx, apache, BIND zone files
40
+ - **Intelligent analysis** — load/disk/memory assessment, project type detection
41
+ - **Conversation persistence** — knowledge tree, coreference resolution
42
+ - **Platform detection** — Linux/macOS/Windows/WSL, adapts commands per OS
43
+ - **Adaptive rules** — learns from failures via LLM
44
+
45
+ ## License
46
+
47
+ MIT — [Dino Bartolome](https://notoken.sh)
@@ -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,5 @@
1
+ /**
2
+ * Plugin system public API.
3
+ */
4
+ export { pluginRegistry } from "./registry.js";
5
+ export type { NotokenPlugin, PluginIntent, PluginPlaybook, PluginHooks, PluginAliases, PluginFileHints, LoadedPlugin, } from "./types.js";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Plugin system public API.
3
+ */
4
+ export { pluginRegistry } from "./registry.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
+ }
@@ -0,0 +1,8 @@
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 {};
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notoken-core",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Shared engine for notoken — NLP parsing, execution, detection, analysis",
5
5
  "type": "module",
6
6
  "license": "MIT",