notoken-core 1.2.1 → 1.4.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 +3 -0
- package/dist/index.js +4 -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/dist/utils/llmManager.d.ts +44 -0
- package/dist/utils/llmManager.js +191 -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
|
@@ -45,8 +45,11 @@ export { formatParsedCommand } from "./utils/output.js";
|
|
|
45
45
|
export { Spinner, withSpinner, progressBar } from "./utils/spinner.js";
|
|
46
46
|
export { createBackup, rollback, listBackups, cleanExpiredBackups, formatBackupList } from "./utils/autoBackup.js";
|
|
47
47
|
export { checkForUpdate, checkForUpdateSync, runUpdate, formatUpdateBanner, type UpdateInfo } from "./utils/updater.js";
|
|
48
|
+
export { detectProviders, formatStatus, goOffline, goOnline, disableLLM, enableLLM, isOfflineMode, isLLMDisabled, recordOfflineCommand, getTokensSaved, formatTokensSaved, formatTokensSavedBrief, saveOnExit, type LLMProvider, type LLMState, } from "./utils/llmManager.js";
|
|
48
49
|
export { logFailure, loadFailures, clearFailures } from "./utils/logger.js";
|
|
49
50
|
export { logUncertainty, loadUncertaintyLog, getUncertaintySummary } from "./nlp/uncertainty.js";
|
|
50
51
|
export { recordHistory, loadHistory, getRecentHistory, searchHistory } from "./context/history.js";
|
|
52
|
+
export { pluginRegistry } from "./plugins/index.js";
|
|
53
|
+
export type { NotokenPlugin, PluginIntent, PluginPlaybook, PluginHooks, LoadedPlugin } from "./plugins/index.js";
|
|
51
54
|
export type { DynamicIntent, ParsedCommand, IntentDef, FieldDef, EnvironmentName } from "./types/intent.js";
|
|
52
55
|
export type { RulePatch, RulePatchChange, FailureLog, RulesConfig } from "./types/rules.js";
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,11 @@ export { Spinner, withSpinner, progressBar } from "./utils/spinner.js";
|
|
|
58
58
|
export { createBackup, rollback, listBackups, cleanExpiredBackups, formatBackupList } from "./utils/autoBackup.js";
|
|
59
59
|
// ── Updates ──
|
|
60
60
|
export { checkForUpdate, checkForUpdateSync, runUpdate, formatUpdateBanner } from "./utils/updater.js";
|
|
61
|
+
// ── LLM Manager ──
|
|
62
|
+
export { detectProviders, formatStatus, goOffline, goOnline, disableLLM, enableLLM, isOfflineMode, isLLMDisabled, recordOfflineCommand, getTokensSaved, formatTokensSaved, formatTokensSavedBrief, saveOnExit, } from "./utils/llmManager.js";
|
|
61
63
|
// ── Logging ──
|
|
62
64
|
export { logFailure, loadFailures, clearFailures } from "./utils/logger.js";
|
|
63
65
|
export { logUncertainty, loadUncertaintyLog, getUncertaintySummary } from "./nlp/uncertainty.js";
|
|
64
66
|
export { recordHistory, loadHistory, getRecentHistory, searchHistory } from "./context/history.js";
|
|
67
|
+
// ── Plugins ──
|
|
68
|
+
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) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Manager.
|
|
3
|
+
*
|
|
4
|
+
* Manages which LLMs are connected, enabled/disabled, and offline mode.
|
|
5
|
+
* Also tracks estimated tokens saved by using deterministic mode.
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* :status — show which LLMs are connected
|
|
9
|
+
* :offline — go fully offline (disable all LLMs)
|
|
10
|
+
* :online — re-enable LLMs
|
|
11
|
+
* :disable <llm> — disable a specific LLM
|
|
12
|
+
* :enable <llm> — enable a specific LLM
|
|
13
|
+
*/
|
|
14
|
+
export interface LLMProvider {
|
|
15
|
+
name: string;
|
|
16
|
+
type: "cli" | "api" | "local";
|
|
17
|
+
available: boolean;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
version?: string;
|
|
20
|
+
model?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface LLMState {
|
|
23
|
+
offlineMode: boolean;
|
|
24
|
+
disabled: string[];
|
|
25
|
+
tokensSaved: number;
|
|
26
|
+
commandsHandledOffline: number;
|
|
27
|
+
lastSaved: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function detectProviders(): LLMProvider[];
|
|
30
|
+
export declare function formatStatus(): string;
|
|
31
|
+
export declare function goOffline(): string;
|
|
32
|
+
export declare function goOnline(): string;
|
|
33
|
+
export declare function disableLLM(name: string): string;
|
|
34
|
+
export declare function enableLLM(name: string): string;
|
|
35
|
+
export declare function isOfflineMode(): boolean;
|
|
36
|
+
export declare function isLLMDisabled(name: string): boolean;
|
|
37
|
+
export declare function recordOfflineCommand(): void;
|
|
38
|
+
export declare function getTokensSaved(): {
|
|
39
|
+
tokens: number;
|
|
40
|
+
commands: number;
|
|
41
|
+
};
|
|
42
|
+
export declare function formatTokensSaved(): string;
|
|
43
|
+
export declare function formatTokensSavedBrief(): string;
|
|
44
|
+
export declare function saveOnExit(): void;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Manager.
|
|
3
|
+
*
|
|
4
|
+
* Manages which LLMs are connected, enabled/disabled, and offline mode.
|
|
5
|
+
* Also tracks estimated tokens saved by using deterministic mode.
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* :status — show which LLMs are connected
|
|
9
|
+
* :offline — go fully offline (disable all LLMs)
|
|
10
|
+
* :online — re-enable LLMs
|
|
11
|
+
* :disable <llm> — disable a specific LLM
|
|
12
|
+
* :enable <llm> — enable a specific LLM
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import { USER_HOME } from "./paths.js";
|
|
18
|
+
const STATE_FILE = resolve(USER_HOME, "llm-state.json");
|
|
19
|
+
const c = {
|
|
20
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
21
|
+
green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m",
|
|
22
|
+
};
|
|
23
|
+
// ─── State Persistence ──────────────────────────────────────────────────────
|
|
24
|
+
function loadState() {
|
|
25
|
+
try {
|
|
26
|
+
if (existsSync(STATE_FILE)) {
|
|
27
|
+
return JSON.parse(readFileSync(STATE_FILE, "utf-8"));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
return { offlineMode: false, disabled: [], tokensSaved: 0, commandsHandledOffline: 0, lastSaved: new Date().toISOString() };
|
|
32
|
+
}
|
|
33
|
+
function saveState(state) {
|
|
34
|
+
try {
|
|
35
|
+
mkdirSync(USER_HOME, { recursive: true });
|
|
36
|
+
state.lastSaved = new Date().toISOString();
|
|
37
|
+
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
}
|
|
41
|
+
let state = loadState();
|
|
42
|
+
// ─── Provider Detection ─────────────────────────────────────────────────────
|
|
43
|
+
export function detectProviders() {
|
|
44
|
+
const providers = [];
|
|
45
|
+
// Claude CLI
|
|
46
|
+
const claudeVersion = tryExec("claude --version");
|
|
47
|
+
providers.push({
|
|
48
|
+
name: "claude",
|
|
49
|
+
type: "cli",
|
|
50
|
+
available: !!claudeVersion,
|
|
51
|
+
enabled: !state.offlineMode && !state.disabled.includes("claude"),
|
|
52
|
+
version: claudeVersion ?? undefined,
|
|
53
|
+
});
|
|
54
|
+
// Ollama
|
|
55
|
+
const ollamaVersion = tryExec("ollama --version");
|
|
56
|
+
let ollamaRunning = false;
|
|
57
|
+
let ollamaModel;
|
|
58
|
+
if (ollamaVersion) {
|
|
59
|
+
try {
|
|
60
|
+
const tags = tryExec("curl -sf --max-time 1 http://localhost:11434/api/tags");
|
|
61
|
+
if (tags) {
|
|
62
|
+
ollamaRunning = true;
|
|
63
|
+
const parsed = JSON.parse(tags);
|
|
64
|
+
ollamaModel = parsed.models?.[0]?.name;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
}
|
|
69
|
+
providers.push({
|
|
70
|
+
name: "ollama",
|
|
71
|
+
type: "local",
|
|
72
|
+
available: ollamaRunning,
|
|
73
|
+
enabled: !state.offlineMode && !state.disabled.includes("ollama"),
|
|
74
|
+
version: ollamaVersion ?? undefined,
|
|
75
|
+
model: ollamaModel,
|
|
76
|
+
});
|
|
77
|
+
// API endpoint
|
|
78
|
+
const apiEndpoint = process.env.NOTOKEN_LLM_ENDPOINT;
|
|
79
|
+
providers.push({
|
|
80
|
+
name: "api",
|
|
81
|
+
type: "api",
|
|
82
|
+
available: !!apiEndpoint,
|
|
83
|
+
enabled: !state.offlineMode && !state.disabled.includes("api") && !!apiEndpoint,
|
|
84
|
+
version: apiEndpoint ? "configured" : undefined,
|
|
85
|
+
model: process.env.NOTOKEN_LLM_MODEL,
|
|
86
|
+
});
|
|
87
|
+
return providers;
|
|
88
|
+
}
|
|
89
|
+
// ─── Status ─────────────────────────────────────────────────────────────────
|
|
90
|
+
export function formatStatus() {
|
|
91
|
+
const providers = detectProviders();
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push(`${c.bold}LLM Status${c.reset}${state.offlineMode ? ` ${c.yellow}[OFFLINE MODE]${c.reset}` : ""}\n`);
|
|
94
|
+
for (const p of providers) {
|
|
95
|
+
const icon = p.available && p.enabled ? `${c.green}⬤${c.reset}` :
|
|
96
|
+
p.available && !p.enabled ? `${c.yellow}⬤${c.reset}` :
|
|
97
|
+
`${c.dim}○${c.reset}`;
|
|
98
|
+
const status = p.available && p.enabled ? `${c.green}active${c.reset}` :
|
|
99
|
+
p.available && !p.enabled ? `${c.yellow}disabled${c.reset}` :
|
|
100
|
+
`${c.dim}not available${c.reset}`;
|
|
101
|
+
const detail = [p.version, p.model].filter(Boolean).join(" / ");
|
|
102
|
+
lines.push(` ${icon} ${c.bold}${p.name}${c.reset} (${p.type}) — ${status}${detail ? ` ${c.dim}${detail}${c.reset}` : ""}`);
|
|
103
|
+
}
|
|
104
|
+
const active = providers.filter(p => p.available && p.enabled);
|
|
105
|
+
lines.push("");
|
|
106
|
+
if (active.length > 0) {
|
|
107
|
+
lines.push(` ${c.dim}Active chain: ${active.map(p => p.name).join(" → ")}${c.reset}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
lines.push(` ${c.dim}Running in deterministic mode (no LLM)${c.reset}`);
|
|
111
|
+
}
|
|
112
|
+
// Token savings
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(` ${c.cyan}Tokens saved:${c.reset} ~${formatTokens(state.tokensSaved)} (${state.commandsHandledOffline} commands handled offline)`);
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
// ─── Controls ───────────────────────────────────────────────────────────────
|
|
118
|
+
export function goOffline() {
|
|
119
|
+
state.offlineMode = true;
|
|
120
|
+
saveState(state);
|
|
121
|
+
return `${c.yellow}Offline mode enabled.${c.reset} All LLM providers disabled. Deterministic engine only.`;
|
|
122
|
+
}
|
|
123
|
+
export function goOnline() {
|
|
124
|
+
state.offlineMode = false;
|
|
125
|
+
saveState(state);
|
|
126
|
+
const providers = detectProviders().filter(p => p.available && p.enabled);
|
|
127
|
+
return `${c.green}Online mode.${c.reset} Active: ${providers.map(p => p.name).join(", ") || "none detected"}`;
|
|
128
|
+
}
|
|
129
|
+
export function disableLLM(name) {
|
|
130
|
+
if (!state.disabled.includes(name)) {
|
|
131
|
+
state.disabled.push(name);
|
|
132
|
+
saveState(state);
|
|
133
|
+
}
|
|
134
|
+
return `${c.yellow}${name} disabled.${c.reset}`;
|
|
135
|
+
}
|
|
136
|
+
export function enableLLM(name) {
|
|
137
|
+
state.disabled = state.disabled.filter(n => n !== name);
|
|
138
|
+
saveState(state);
|
|
139
|
+
return `${c.green}${name} enabled.${c.reset}`;
|
|
140
|
+
}
|
|
141
|
+
export function isOfflineMode() {
|
|
142
|
+
return state.offlineMode;
|
|
143
|
+
}
|
|
144
|
+
export function isLLMDisabled(name) {
|
|
145
|
+
return state.offlineMode || state.disabled.includes(name);
|
|
146
|
+
}
|
|
147
|
+
// ─── Token Savings Tracker ──────────────────────────────────────────────────
|
|
148
|
+
// Rough estimate: average LLM call uses ~500 tokens input + ~200 output
|
|
149
|
+
const AVG_TOKENS_PER_CALL = 700;
|
|
150
|
+
export function recordOfflineCommand() {
|
|
151
|
+
state.commandsHandledOffline++;
|
|
152
|
+
state.tokensSaved += AVG_TOKENS_PER_CALL;
|
|
153
|
+
// Save periodically (every 10 commands)
|
|
154
|
+
if (state.commandsHandledOffline % 10 === 0)
|
|
155
|
+
saveState(state);
|
|
156
|
+
}
|
|
157
|
+
export function getTokensSaved() {
|
|
158
|
+
return { tokens: state.tokensSaved, commands: state.commandsHandledOffline };
|
|
159
|
+
}
|
|
160
|
+
export function formatTokensSaved() {
|
|
161
|
+
const { tokens, commands } = getTokensSaved();
|
|
162
|
+
if (commands === 0)
|
|
163
|
+
return "";
|
|
164
|
+
return `${c.dim}~${formatTokens(tokens)} tokens saved (${commands} commands handled offline)${c.reset}`;
|
|
165
|
+
}
|
|
166
|
+
export function formatTokensSavedBrief() {
|
|
167
|
+
const { tokens, commands } = getTokensSaved();
|
|
168
|
+
if (commands === 0)
|
|
169
|
+
return "";
|
|
170
|
+
return `${c.dim}Tokens saved: ~${formatTokens(tokens)}${c.reset}`;
|
|
171
|
+
}
|
|
172
|
+
// Save state on exit
|
|
173
|
+
export function saveOnExit() {
|
|
174
|
+
saveState(state);
|
|
175
|
+
}
|
|
176
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
177
|
+
function formatTokens(n) {
|
|
178
|
+
if (n < 1000)
|
|
179
|
+
return String(n);
|
|
180
|
+
if (n < 1_000_000)
|
|
181
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
182
|
+
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
183
|
+
}
|
|
184
|
+
function tryExec(cmd) {
|
|
185
|
+
try {
|
|
186
|
+
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim() || null;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|