notoken-core 1.6.0 → 1.8.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/config/ascii-art.json +12 -0
- package/config/chat-responses.json +1019 -0
- package/config/cheat-sheets.json +94 -0
- package/config/concept-clusters.json +31 -0
- package/config/daily-tips.json +105 -0
- package/config/entities.json +93 -0
- package/config/history-today.json +9762 -0
- package/config/image-prompts.json +20 -0
- package/config/intent-vectors.json +1 -0
- package/config/intents.json +5354 -85
- package/config/ollama-models.json +193 -0
- package/config/rules.json +32 -1
- package/config/startup-quotes.json +45 -0
- package/dist/automation/discordPatchright.d.ts +35 -0
- package/dist/automation/discordPatchright.js +424 -0
- package/dist/automation/discordSetup.d.ts +31 -0
- package/dist/automation/discordSetup.js +338 -0
- package/dist/automation/smAutomation.d.ts +82 -0
- package/dist/automation/smAutomation.js +448 -0
- package/dist/conversation/coreference.js +44 -4
- package/dist/conversation/pendingActions.d.ts +55 -0
- package/dist/conversation/pendingActions.js +127 -0
- package/dist/conversation/store.d.ts +72 -0
- package/dist/conversation/store.js +140 -1
- package/dist/conversation/topicTracker.d.ts +36 -0
- package/dist/conversation/topicTracker.js +141 -0
- package/dist/execution/ssh.d.ts +42 -1
- package/dist/execution/ssh.js +538 -3
- package/dist/handlers/executor.d.ts +2 -0
- package/dist/handlers/executor.js +4234 -31
- package/dist/index.d.ts +35 -4
- package/dist/index.js +51 -3
- package/dist/nlp/batchParser.d.ts +30 -0
- package/dist/nlp/batchParser.js +77 -0
- package/dist/nlp/conceptExpansion.d.ts +54 -0
- package/dist/nlp/conceptExpansion.js +136 -0
- package/dist/nlp/conceptRouter.d.ts +49 -0
- package/dist/nlp/conceptRouter.js +302 -0
- package/dist/nlp/confidenceCalibrator.d.ts +62 -0
- package/dist/nlp/confidenceCalibrator.js +116 -0
- package/dist/nlp/correctionLearner.d.ts +45 -0
- package/dist/nlp/correctionLearner.js +207 -0
- package/dist/nlp/entitySpellCorrect.d.ts +35 -0
- package/dist/nlp/entitySpellCorrect.js +141 -0
- package/dist/nlp/knowledgeGraph.d.ts +70 -0
- package/dist/nlp/knowledgeGraph.js +380 -0
- package/dist/nlp/llmFallback.js +28 -1
- package/dist/nlp/multiClassifier.js +91 -6
- package/dist/nlp/multiIntent.d.ts +43 -0
- package/dist/nlp/multiIntent.js +154 -0
- package/dist/nlp/parseIntent.d.ts +6 -1
- package/dist/nlp/parseIntent.js +180 -5
- package/dist/nlp/ruleParser.js +317 -0
- package/dist/nlp/semanticSimilarity.d.ts +30 -0
- package/dist/nlp/semanticSimilarity.js +174 -0
- package/dist/nlp/vocabularyBuilder.d.ts +43 -0
- package/dist/nlp/vocabularyBuilder.js +224 -0
- package/dist/nlp/wikidata.d.ts +49 -0
- package/dist/nlp/wikidata.js +228 -0
- package/dist/policy/confirm.d.ts +10 -0
- package/dist/policy/confirm.js +39 -0
- package/dist/policy/safety.js +6 -4
- package/dist/types/intent.d.ts +8 -0
- package/dist/types/intent.js +1 -0
- package/dist/utils/achievements.d.ts +38 -0
- package/dist/utils/achievements.js +126 -0
- package/dist/utils/aliases.d.ts +5 -0
- package/dist/utils/aliases.js +39 -0
- package/dist/utils/analysis.js +71 -15
- package/dist/utils/bookmarks.d.ts +13 -0
- package/dist/utils/bookmarks.js +51 -0
- package/dist/utils/browser.d.ts +64 -0
- package/dist/utils/browser.js +364 -0
- package/dist/utils/commandHistory.d.ts +20 -0
- package/dist/utils/commandHistory.js +108 -0
- package/dist/utils/completer.d.ts +17 -0
- package/dist/utils/completer.js +79 -0
- package/dist/utils/config.js +32 -2
- package/dist/utils/dbQuery.d.ts +25 -0
- package/dist/utils/dbQuery.js +248 -0
- package/dist/utils/devTools.d.ts +35 -0
- package/dist/utils/devTools.js +95 -0
- package/dist/utils/discordDiag.d.ts +35 -0
- package/dist/utils/discordDiag.js +826 -0
- package/dist/utils/diskCleanup.d.ts +36 -0
- package/dist/utils/diskCleanup.js +775 -0
- package/dist/utils/entityResolver.d.ts +107 -0
- package/dist/utils/entityResolver.js +468 -0
- package/dist/utils/imageGen.d.ts +92 -0
- package/dist/utils/imageGen.js +2031 -0
- package/dist/utils/installTracker.d.ts +57 -0
- package/dist/utils/installTracker.js +160 -0
- package/dist/utils/multiExec.d.ts +21 -0
- package/dist/utils/multiExec.js +141 -0
- package/dist/utils/openclawDiag.d.ts +29 -0
- package/dist/utils/openclawDiag.js +1035 -0
- package/dist/utils/output.js +4 -0
- package/dist/utils/platform.js +2 -1
- package/dist/utils/progressReporter.d.ts +50 -0
- package/dist/utils/progressReporter.js +58 -0
- package/dist/utils/projectDetect.d.ts +44 -0
- package/dist/utils/projectDetect.js +319 -0
- package/dist/utils/projectScanner.d.ts +44 -0
- package/dist/utils/projectScanner.js +312 -0
- package/dist/utils/shellCompat.d.ts +78 -0
- package/dist/utils/shellCompat.js +186 -0
- package/dist/utils/smartArchive.d.ts +16 -0
- package/dist/utils/smartArchive.js +172 -0
- package/dist/utils/smartRetry.d.ts +26 -0
- package/dist/utils/smartRetry.js +114 -0
- package/dist/utils/snippets.d.ts +13 -0
- package/dist/utils/snippets.js +53 -0
- package/dist/utils/stabilityMatrixManager.d.ts +80 -0
- package/dist/utils/stabilityMatrixManager.js +268 -0
- package/dist/utils/teachMode.d.ts +41 -0
- package/dist/utils/teachMode.js +100 -0
- package/dist/utils/timer.d.ts +22 -0
- package/dist/utils/timer.js +52 -0
- package/dist/utils/updater.d.ts +1 -0
- package/dist/utils/updater.js +1 -1
- package/dist/utils/version.d.ts +20 -0
- package/dist/utils/version.js +212 -0
- package/package.json +6 -3
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Manager.
|
|
3
|
+
*
|
|
4
|
+
* Detects, installs, and launches browser automation engines.
|
|
5
|
+
*
|
|
6
|
+
* Priority:
|
|
7
|
+
* 1. Patchright (patched Playwright — anti-detection)
|
|
8
|
+
* 2. Playwright
|
|
9
|
+
* 3. Docker (browserless/chromium container)
|
|
10
|
+
* 4. System browser (xdg-open / open / start)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* notoken browse <url>
|
|
14
|
+
* notoken browse install
|
|
15
|
+
* notoken browse status
|
|
16
|
+
* "open google.com"
|
|
17
|
+
* "take screenshot of example.com"
|
|
18
|
+
* "browse to localhost:3000"
|
|
19
|
+
*/
|
|
20
|
+
import { execSync, spawn } from "node:child_process";
|
|
21
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { USER_HOME } from "./paths.js";
|
|
24
|
+
import { platform } from "node:os";
|
|
25
|
+
const SCREENSHOTS_DIR = resolve(USER_HOME, "screenshots");
|
|
26
|
+
const c = {
|
|
27
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
28
|
+
green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m",
|
|
29
|
+
};
|
|
30
|
+
// ─── Detection ─────────────────────────────────────────────────────────────
|
|
31
|
+
function tryExec(cmd, timeout = 5000) {
|
|
32
|
+
try {
|
|
33
|
+
return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout }).trim() || null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function hasNpmPackage(name) {
|
|
40
|
+
// Fast check: try to resolve the module directly
|
|
41
|
+
const resolved = tryExec(`node -e "try{require.resolve('${name}');console.log('ok')}catch{}" 2>/dev/null`, 3000);
|
|
42
|
+
if (resolved === "ok")
|
|
43
|
+
return true;
|
|
44
|
+
// Check global bin
|
|
45
|
+
if (tryExec(`which ${name} 2>/dev/null`, 2000))
|
|
46
|
+
return true;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
export function detectBrowserEngines() {
|
|
50
|
+
const engines = [];
|
|
51
|
+
// 1. Patchright
|
|
52
|
+
const patchrightInstalled = hasNpmPackage("patchright");
|
|
53
|
+
engines.push({
|
|
54
|
+
engine: "patchright",
|
|
55
|
+
available: patchrightInstalled,
|
|
56
|
+
version: patchrightInstalled ? (tryExec("npx patchright --version 2>/dev/null", 3000) ?? "installed") : undefined,
|
|
57
|
+
browsersInstalled: patchrightInstalled ? checkBrowserBinaries("patchright") : false,
|
|
58
|
+
});
|
|
59
|
+
// 2. Playwright
|
|
60
|
+
const playwrightInstalled = hasNpmPackage("playwright");
|
|
61
|
+
engines.push({
|
|
62
|
+
engine: "playwright",
|
|
63
|
+
available: playwrightInstalled,
|
|
64
|
+
version: playwrightInstalled ? (tryExec("npx playwright --version 2>/dev/null", 3000) ?? "installed") : undefined,
|
|
65
|
+
browsersInstalled: playwrightInstalled ? checkBrowserBinaries("playwright") : false,
|
|
66
|
+
});
|
|
67
|
+
// 3. Docker (browserless)
|
|
68
|
+
const dockerAvailable = !!tryExec("docker --version");
|
|
69
|
+
let dockerImage;
|
|
70
|
+
if (dockerAvailable) {
|
|
71
|
+
const images = tryExec("docker images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null");
|
|
72
|
+
if (images) {
|
|
73
|
+
const browserless = images.split("\n").find(i => i.includes("browserless") || i.includes("chromium") || i.includes("chrome"));
|
|
74
|
+
dockerImage = browserless;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
engines.push({
|
|
78
|
+
engine: "docker",
|
|
79
|
+
available: dockerAvailable,
|
|
80
|
+
version: dockerAvailable ? tryExec("docker --version")?.replace("Docker version ", "") ?? undefined : undefined,
|
|
81
|
+
dockerImage,
|
|
82
|
+
});
|
|
83
|
+
// 4. System browser (always available)
|
|
84
|
+
engines.push({
|
|
85
|
+
engine: "system",
|
|
86
|
+
available: true,
|
|
87
|
+
version: getSystemBrowserName(),
|
|
88
|
+
});
|
|
89
|
+
return engines;
|
|
90
|
+
}
|
|
91
|
+
function checkBrowserBinaries(engine) {
|
|
92
|
+
// Check if chromium is installed for the engine
|
|
93
|
+
const check = tryExec(`npx ${engine} install --dry-run chromium 2>&1`);
|
|
94
|
+
if (check?.includes("already installed") || check?.includes("is already"))
|
|
95
|
+
return true;
|
|
96
|
+
// Fallback: try to find the browser cache
|
|
97
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
98
|
+
const cacheDir = engine === "patchright"
|
|
99
|
+
? resolve(home, ".cache", "patchright")
|
|
100
|
+
: resolve(home, ".cache", "ms-playwright");
|
|
101
|
+
return existsSync(cacheDir);
|
|
102
|
+
}
|
|
103
|
+
function getSystemBrowserName() {
|
|
104
|
+
const os = platform();
|
|
105
|
+
if (os === "darwin")
|
|
106
|
+
return "macOS default (open)";
|
|
107
|
+
if (os === "win32")
|
|
108
|
+
return "Windows default (start)";
|
|
109
|
+
// Check for common Linux browsers
|
|
110
|
+
for (const browser of ["google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "firefox"]) {
|
|
111
|
+
if (tryExec(`which ${browser}`))
|
|
112
|
+
return browser;
|
|
113
|
+
}
|
|
114
|
+
return "xdg-open";
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get the best available engine.
|
|
118
|
+
*/
|
|
119
|
+
export function getBestEngine() {
|
|
120
|
+
const engines = detectBrowserEngines();
|
|
121
|
+
// Prefer patchright > playwright > docker (only with image) > system
|
|
122
|
+
const patchright = engines.find(e => e.engine === "patchright" && e.available && e.browsersInstalled);
|
|
123
|
+
if (patchright)
|
|
124
|
+
return patchright;
|
|
125
|
+
const playwright = engines.find(e => e.engine === "playwright" && e.available && e.browsersInstalled);
|
|
126
|
+
if (playwright)
|
|
127
|
+
return playwright;
|
|
128
|
+
// Patchright/playwright installed but no browsers — still usable (will auto-download)
|
|
129
|
+
const patchrightNoBrowser = engines.find(e => e.engine === "patchright" && e.available);
|
|
130
|
+
if (patchrightNoBrowser)
|
|
131
|
+
return patchrightNoBrowser;
|
|
132
|
+
const playwrightNoBrowser = engines.find(e => e.engine === "playwright" && e.available);
|
|
133
|
+
if (playwrightNoBrowser)
|
|
134
|
+
return playwrightNoBrowser;
|
|
135
|
+
const docker = engines.find(e => e.engine === "docker" && e.available && e.dockerImage);
|
|
136
|
+
if (docker)
|
|
137
|
+
return docker;
|
|
138
|
+
return engines.find(e => e.engine === "system") ?? null;
|
|
139
|
+
}
|
|
140
|
+
export async function installBrowserEngine(engine) {
|
|
141
|
+
const target = engine ?? "patchright";
|
|
142
|
+
if (target === "patchright" || target === "playwright") {
|
|
143
|
+
try {
|
|
144
|
+
console.log(`${c.dim}Installing ${target}...${c.reset}`);
|
|
145
|
+
execSync(`npm install -g ${target}`, { stdio: "inherit", timeout: 120000 });
|
|
146
|
+
console.log(`${c.dim}Installing ${target} browsers (chromium)...${c.reset}`);
|
|
147
|
+
execSync(`npx ${target} install chromium`, { stdio: "inherit", timeout: 300000 });
|
|
148
|
+
return { success: true, engine: target, message: `${target} + chromium installed successfully` };
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return { success: false, engine: target, message: `Failed to install ${target}: ${err instanceof Error ? err.message : err}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (target === "docker") {
|
|
155
|
+
const dockerAvailable = !!tryExec("docker --version");
|
|
156
|
+
if (!dockerAvailable) {
|
|
157
|
+
return { success: false, engine: "docker", message: "Docker is not installed. Install Docker first." };
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
console.log(`${c.dim}Pulling browserless/chromium image...${c.reset}`);
|
|
161
|
+
execSync("docker pull ghcr.io/browserless/chromium", { stdio: "inherit", timeout: 300000 });
|
|
162
|
+
return { success: true, engine: "docker", message: "browserless/chromium image pulled" };
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return { success: false, engine: "docker", message: `Failed to pull image: ${err instanceof Error ? err.message : err}` };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { success: true, engine: "system", message: "System browser is always available" };
|
|
169
|
+
}
|
|
170
|
+
// ─── Browse ────────────────────────────────────────────────────────────────
|
|
171
|
+
/**
|
|
172
|
+
* Open a URL using the best available engine.
|
|
173
|
+
*/
|
|
174
|
+
export async function browse(opts) {
|
|
175
|
+
const url = normalizeUrl(opts.url);
|
|
176
|
+
const engine = getBestEngine();
|
|
177
|
+
if (!engine) {
|
|
178
|
+
return { engine: "system", url, error: "No browser engine available" };
|
|
179
|
+
}
|
|
180
|
+
// For system browser or non-headless without screenshot, just open
|
|
181
|
+
if (engine.engine === "system" && !opts.screenshot) {
|
|
182
|
+
return openSystemBrowser(url);
|
|
183
|
+
}
|
|
184
|
+
// For automation engines
|
|
185
|
+
if (engine.engine === "patchright" || engine.engine === "playwright") {
|
|
186
|
+
return browseWithPlaywright(engine.engine, url, opts);
|
|
187
|
+
}
|
|
188
|
+
if (engine.engine === "docker") {
|
|
189
|
+
return browseWithDocker(url, opts);
|
|
190
|
+
}
|
|
191
|
+
return openSystemBrowser(url);
|
|
192
|
+
}
|
|
193
|
+
export function normalizeUrl(url) {
|
|
194
|
+
// Add https:// if no protocol
|
|
195
|
+
if (!/^https?:\/\//i.test(url) && !url.startsWith("file://")) {
|
|
196
|
+
// If it looks like localhost, use http
|
|
197
|
+
if (url.startsWith("localhost") || url.startsWith("127.0.0.1") || url.startsWith("0.0.0.0")) {
|
|
198
|
+
return `http://${url}`;
|
|
199
|
+
}
|
|
200
|
+
return `https://${url}`;
|
|
201
|
+
}
|
|
202
|
+
return url;
|
|
203
|
+
}
|
|
204
|
+
function openSystemBrowser(url) {
|
|
205
|
+
try {
|
|
206
|
+
const os = platform();
|
|
207
|
+
if (os === "darwin") {
|
|
208
|
+
execSync(`open "${url}"`, { stdio: "ignore" });
|
|
209
|
+
}
|
|
210
|
+
else if (os === "win32") {
|
|
211
|
+
execSync(`start "" "${url}"`, { stdio: "ignore", shell: "cmd.exe" });
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// WSL check
|
|
215
|
+
const isWSL = tryExec("grep -qi microsoft /proc/version && echo wsl");
|
|
216
|
+
if (isWSL) {
|
|
217
|
+
execSync(`cmd.exe /c start "" "${url}"`, { stdio: "ignore" });
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { engine: "system", url, title: "Opened in system browser" };
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
return { engine: "system", url, error: `Failed to open: ${err instanceof Error ? err.message : err}` };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function browseWithPlaywright(engine, url, opts) {
|
|
230
|
+
// Generate a script and run it via node
|
|
231
|
+
const headless = opts.headless ?? !opts.screenshot;
|
|
232
|
+
const viewport = opts.viewport ?? { width: 1280, height: 720 };
|
|
233
|
+
const waitFor = opts.waitFor ?? 2000;
|
|
234
|
+
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
235
|
+
const screenshotPath = opts.screenshotPath ?? opts.screenshot
|
|
236
|
+
? resolve(SCREENSHOTS_DIR, `screenshot-${Date.now()}.png`)
|
|
237
|
+
: undefined;
|
|
238
|
+
const script = `
|
|
239
|
+
const { chromium } = require("${engine}");
|
|
240
|
+
(async () => {
|
|
241
|
+
const browser = await chromium.launch({ headless: ${headless} });
|
|
242
|
+
const context = await browser.newContext({
|
|
243
|
+
viewport: { width: ${viewport.width}, height: ${viewport.height} },
|
|
244
|
+
${opts.userAgent ? `userAgent: "${opts.userAgent}",` : ""}
|
|
245
|
+
});
|
|
246
|
+
const page = await context.newPage();
|
|
247
|
+
await page.goto("${url}", { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
248
|
+
await page.waitForTimeout(${waitFor});
|
|
249
|
+
const title = await page.title();
|
|
250
|
+
${screenshotPath ? `await page.screenshot({ path: "${screenshotPath}", fullPage: true });` : ""}
|
|
251
|
+
${!headless && !opts.screenshot ? `
|
|
252
|
+
// Keep browser open for interactive use
|
|
253
|
+
console.log(JSON.stringify({ title, status: "open" }));
|
|
254
|
+
// Wait for user to close
|
|
255
|
+
await new Promise(() => {});
|
|
256
|
+
` : `
|
|
257
|
+
console.log(JSON.stringify({ title, status: "done" }));
|
|
258
|
+
await browser.close();
|
|
259
|
+
`}
|
|
260
|
+
})().catch(err => {
|
|
261
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
|
264
|
+
`;
|
|
265
|
+
const tmpScript = resolve(USER_HOME, ".browse-script.cjs");
|
|
266
|
+
writeFileSync(tmpScript, script);
|
|
267
|
+
return new Promise((res) => {
|
|
268
|
+
const child = spawn("node", [tmpScript], {
|
|
269
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
270
|
+
timeout: opts.screenshot ? 60000 : 0,
|
|
271
|
+
});
|
|
272
|
+
let stdout = "";
|
|
273
|
+
let stderr = "";
|
|
274
|
+
child.stdout?.on("data", (d) => { stdout += d.toString(); });
|
|
275
|
+
child.stderr?.on("data", (d) => { stderr += d.toString(); });
|
|
276
|
+
// For interactive (non-headless, no screenshot), resolve immediately
|
|
277
|
+
if (!headless && !opts.screenshot) {
|
|
278
|
+
setTimeout(() => {
|
|
279
|
+
res({ engine, url, title: "Browser opened interactively", screenshotPath: undefined });
|
|
280
|
+
}, 3000);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
child.on("close", () => {
|
|
284
|
+
try {
|
|
285
|
+
const result = JSON.parse(stdout.trim().split("\n").pop() ?? "{}");
|
|
286
|
+
if (result.error) {
|
|
287
|
+
res({ engine, url, error: result.error });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
res({ engine, url, title: result.title, screenshotPath });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
res({ engine, url, error: stderr || "Unknown error" });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function browseWithDocker(url, opts) {
|
|
300
|
+
const screenshotPath = opts.screenshot
|
|
301
|
+
? resolve(SCREENSHOTS_DIR, `screenshot-${Date.now()}.png`)
|
|
302
|
+
: undefined;
|
|
303
|
+
try {
|
|
304
|
+
// Start browserless container if not running
|
|
305
|
+
const running = tryExec("docker ps --format '{{.Image}}' 2>/dev/null");
|
|
306
|
+
if (!running?.includes("browserless")) {
|
|
307
|
+
console.log(`${c.dim}Starting browserless container...${c.reset}`);
|
|
308
|
+
execSync("docker run -d --rm -p 3100:3000 --name notoken-browser ghcr.io/browserless/chromium", {
|
|
309
|
+
stdio: "ignore",
|
|
310
|
+
timeout: 30000,
|
|
311
|
+
});
|
|
312
|
+
// Wait for it to be ready
|
|
313
|
+
execSync("sleep 2");
|
|
314
|
+
}
|
|
315
|
+
if (opts.screenshot && screenshotPath) {
|
|
316
|
+
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
|
317
|
+
// Use browserless screenshot API
|
|
318
|
+
execSync(`curl -sf -o "${screenshotPath}" "http://localhost:3100/screenshot?url=${encodeURIComponent(url)}"`, {
|
|
319
|
+
timeout: 30000,
|
|
320
|
+
});
|
|
321
|
+
return { engine: "docker", url, title: "Screenshot via Docker", screenshotPath };
|
|
322
|
+
}
|
|
323
|
+
// Just open — use browserless content API to get title
|
|
324
|
+
const content = tryExec(`curl -sf "http://localhost:3100/content?url=${encodeURIComponent(url)}" 2>/dev/null`);
|
|
325
|
+
const titleMatch = content?.match(/<title>([^<]+)<\/title>/i);
|
|
326
|
+
return { engine: "docker", url, title: titleMatch?.[1] ?? "Page loaded via Docker" };
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
return { engine: "docker", url, error: `Docker browse failed: ${err instanceof Error ? err.message : err}` };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// ─── Formatting ────────────────────────────────────────────────────────────
|
|
333
|
+
export function formatBrowserStatus(engines) {
|
|
334
|
+
const lines = [];
|
|
335
|
+
lines.push(`${c.bold}Browser Engines${c.reset}\n`);
|
|
336
|
+
for (const e of engines) {
|
|
337
|
+
const icon = e.available
|
|
338
|
+
? (e.engine === "system" ? `${c.green}⬤${c.reset}` :
|
|
339
|
+
e.browsersInstalled !== false ? `${c.green}⬤${c.reset}` : `${c.yellow}⬤${c.reset}`)
|
|
340
|
+
: `${c.dim}○${c.reset}`;
|
|
341
|
+
const status = e.available
|
|
342
|
+
? (e.browsersInstalled === false ? `${c.yellow}installed (no browsers)${c.reset}` : `${c.green}ready${c.reset}`)
|
|
343
|
+
: `${c.dim}not installed${c.reset}`;
|
|
344
|
+
const ver = e.version ? ` ${c.dim}${e.version}${c.reset}` : "";
|
|
345
|
+
const docker = e.dockerImage ? ` ${c.dim}image: ${e.dockerImage}${c.reset}` : "";
|
|
346
|
+
const pref = e === getBestEngine() ? ` ${c.cyan}← active${c.reset}` : "";
|
|
347
|
+
lines.push(` ${icon} ${c.bold}${e.engine}${c.reset} — ${status}${ver}${docker}${pref}`);
|
|
348
|
+
}
|
|
349
|
+
const best = getBestEngine();
|
|
350
|
+
lines.push("");
|
|
351
|
+
lines.push(` ${c.dim}Active engine: ${best?.engine ?? "none"}${c.reset}`);
|
|
352
|
+
lines.push(` ${c.dim}Screenshots: ${SCREENSHOTS_DIR}${c.reset}`);
|
|
353
|
+
return lines.join("\n");
|
|
354
|
+
}
|
|
355
|
+
// ─── Stop Docker browser ───────────────────────────────────────────────────
|
|
356
|
+
export function stopDockerBrowser() {
|
|
357
|
+
try {
|
|
358
|
+
execSync("docker stop notoken-browser 2>/dev/null", { stdio: "ignore", timeout: 10000 });
|
|
359
|
+
return `${c.green}✓${c.reset} Docker browser stopped.`;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return `${c.dim}No Docker browser running.${c.reset}`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command History — persistent history with search.
|
|
3
|
+
*
|
|
4
|
+
* Stores every command typed in interactive mode.
|
|
5
|
+
* Supports:
|
|
6
|
+
* - History file (~/.notoken/command-history.txt)
|
|
7
|
+
* - Search (Ctrl+R style fuzzy search)
|
|
8
|
+
* - Recent commands for suggestions
|
|
9
|
+
* - Dedup consecutive duplicates
|
|
10
|
+
*/
|
|
11
|
+
/** Load history from disk. */
|
|
12
|
+
export declare function loadHistory(): string[];
|
|
13
|
+
/** Add a command to history (dedup consecutive). */
|
|
14
|
+
export declare function addToHistory(command: string): void;
|
|
15
|
+
/** Search history with fuzzy matching. */
|
|
16
|
+
export declare function searchHistory(query: string, limit?: number): string[];
|
|
17
|
+
/** Get the N most recent unique commands. */
|
|
18
|
+
export declare function getRecentCommands(limit?: number): string[];
|
|
19
|
+
/** Get history for readline (returns copy of array for rl.history). */
|
|
20
|
+
export declare function getReadlineHistory(): string[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command History — persistent history with search.
|
|
3
|
+
*
|
|
4
|
+
* Stores every command typed in interactive mode.
|
|
5
|
+
* Supports:
|
|
6
|
+
* - History file (~/.notoken/command-history.txt)
|
|
7
|
+
* - Search (Ctrl+R style fuzzy search)
|
|
8
|
+
* - Recent commands for suggestions
|
|
9
|
+
* - Dedup consecutive duplicates
|
|
10
|
+
*/
|
|
11
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
const HISTORY_DIR = resolve(homedir(), ".notoken");
|
|
15
|
+
const HISTORY_FILE = resolve(HISTORY_DIR, "command-history.txt");
|
|
16
|
+
const MAX_HISTORY = 2000;
|
|
17
|
+
let _history = null;
|
|
18
|
+
/** Load history from disk. */
|
|
19
|
+
export function loadHistory() {
|
|
20
|
+
if (_history)
|
|
21
|
+
return _history;
|
|
22
|
+
if (!existsSync(HISTORY_DIR))
|
|
23
|
+
mkdirSync(HISTORY_DIR, { recursive: true });
|
|
24
|
+
if (existsSync(HISTORY_FILE)) {
|
|
25
|
+
_history = readFileSync(HISTORY_FILE, "utf-8")
|
|
26
|
+
.split("\n")
|
|
27
|
+
.filter(l => l.trim())
|
|
28
|
+
.slice(-MAX_HISTORY);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
_history = [];
|
|
32
|
+
}
|
|
33
|
+
return _history;
|
|
34
|
+
}
|
|
35
|
+
/** Add a command to history (dedup consecutive). */
|
|
36
|
+
export function addToHistory(command) {
|
|
37
|
+
const history = loadHistory();
|
|
38
|
+
const trimmed = command.trim();
|
|
39
|
+
if (!trimmed)
|
|
40
|
+
return;
|
|
41
|
+
// Don't add if it's the same as the last command
|
|
42
|
+
if (history.length > 0 && history[history.length - 1] === trimmed)
|
|
43
|
+
return;
|
|
44
|
+
// Don't add meta commands
|
|
45
|
+
if (trimmed.startsWith(":") || trimmed.startsWith("/"))
|
|
46
|
+
return;
|
|
47
|
+
history.push(trimmed);
|
|
48
|
+
// Append to file
|
|
49
|
+
try {
|
|
50
|
+
appendFileSync(HISTORY_FILE, trimmed + "\n");
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
// Trim in memory if too long
|
|
54
|
+
if (history.length > MAX_HISTORY) {
|
|
55
|
+
_history = history.slice(-MAX_HISTORY);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Search history with fuzzy matching. */
|
|
59
|
+
export function searchHistory(query, limit = 10) {
|
|
60
|
+
const history = loadHistory();
|
|
61
|
+
const lower = query.toLowerCase();
|
|
62
|
+
const matches = [];
|
|
63
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
64
|
+
const cmd = history[i];
|
|
65
|
+
const cmdLower = cmd.toLowerCase();
|
|
66
|
+
if (cmdLower.includes(lower)) {
|
|
67
|
+
// Exact substring match — higher score for more recent
|
|
68
|
+
const recency = (i / history.length) * 0.3; // 0-0.3
|
|
69
|
+
const relevance = lower.length / cmdLower.length; // longer match = better
|
|
70
|
+
matches.push({ cmd, score: 0.5 + recency + relevance * 0.2 });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Check word overlap
|
|
74
|
+
const queryWords = lower.split(/\s+/);
|
|
75
|
+
const cmdWords = new Set(cmdLower.split(/\s+/));
|
|
76
|
+
const overlap = queryWords.filter(w => cmdWords.has(w)).length;
|
|
77
|
+
if (overlap > 0) {
|
|
78
|
+
const score = (overlap / queryWords.length) * 0.4 + (i / history.length) * 0.2;
|
|
79
|
+
matches.push({ cmd, score });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Dedup and sort by score
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
return matches
|
|
86
|
+
.sort((a, b) => b.score - a.score)
|
|
87
|
+
.filter(m => { if (seen.has(m.cmd))
|
|
88
|
+
return false; seen.add(m.cmd); return true; })
|
|
89
|
+
.slice(0, limit)
|
|
90
|
+
.map(m => m.cmd);
|
|
91
|
+
}
|
|
92
|
+
/** Get the N most recent unique commands. */
|
|
93
|
+
export function getRecentCommands(limit = 10) {
|
|
94
|
+
const history = loadHistory();
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
const recent = [];
|
|
97
|
+
for (let i = history.length - 1; i >= 0 && recent.length < limit; i--) {
|
|
98
|
+
if (!seen.has(history[i])) {
|
|
99
|
+
seen.add(history[i]);
|
|
100
|
+
recent.push(history[i]);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return recent;
|
|
104
|
+
}
|
|
105
|
+
/** Get history for readline (returns copy of array for rl.history). */
|
|
106
|
+
export function getReadlineHistory() {
|
|
107
|
+
return [...loadHistory()].reverse();
|
|
108
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab-completion for notoken interactive mode.
|
|
3
|
+
*
|
|
4
|
+
* Builds a flat list of completable phrases from intents, meta commands,
|
|
5
|
+
* service aliases, common verbs, and recent history. Cached and rebuilt
|
|
6
|
+
* every 60 seconds.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Build (or rebuild) the flat list of completable strings.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildCompletions(): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Readline-compatible completer function.
|
|
14
|
+
*
|
|
15
|
+
* Signature: (line: string) => [matches: string[], line: string]
|
|
16
|
+
*/
|
|
17
|
+
export declare function completeInput(line: string): [string[], string];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab-completion for notoken interactive mode.
|
|
3
|
+
*
|
|
4
|
+
* Builds a flat list of completable phrases from intents, meta commands,
|
|
5
|
+
* service aliases, common verbs, and recent history. Cached and rebuilt
|
|
6
|
+
* every 60 seconds.
|
|
7
|
+
*/
|
|
8
|
+
import { loadIntents } from "./config.js";
|
|
9
|
+
import { loadRules } from "./config.js";
|
|
10
|
+
import { getRecentHistory } from "../context/history.js";
|
|
11
|
+
// ── Cache ───────────────────────────────────────────────────────────────────
|
|
12
|
+
let cached = [];
|
|
13
|
+
let lastBuilt = 0;
|
|
14
|
+
const CACHE_TTL = 60_000; // 60 s
|
|
15
|
+
const META_COMMANDS = [
|
|
16
|
+
"/jobs", "/help", "/quit", "/output", "/kill",
|
|
17
|
+
"/aliases", "/history",
|
|
18
|
+
];
|
|
19
|
+
const COMMON_VERBS = [
|
|
20
|
+
"restart", "check", "show", "list", "install",
|
|
21
|
+
"diagnose", "monitor",
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Build (or rebuild) the flat list of completable strings.
|
|
25
|
+
*/
|
|
26
|
+
export function buildCompletions() {
|
|
27
|
+
const set = new Set();
|
|
28
|
+
// Meta commands
|
|
29
|
+
for (const cmd of META_COMMANDS)
|
|
30
|
+
set.add(cmd);
|
|
31
|
+
// Common verbs
|
|
32
|
+
for (const v of COMMON_VERBS)
|
|
33
|
+
set.add(v);
|
|
34
|
+
// Intent synonyms
|
|
35
|
+
try {
|
|
36
|
+
const intents = loadIntents();
|
|
37
|
+
for (const intent of intents) {
|
|
38
|
+
for (const syn of intent.synonyms ?? []) {
|
|
39
|
+
set.add(syn.toLowerCase());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch { /* config may not be loaded yet */ }
|
|
44
|
+
// Service alias names
|
|
45
|
+
try {
|
|
46
|
+
const rules = loadRules();
|
|
47
|
+
for (const [service, aliases] of Object.entries(rules.serviceAliases)) {
|
|
48
|
+
set.add(service);
|
|
49
|
+
for (const a of aliases)
|
|
50
|
+
set.add(a.toLowerCase());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* rules may not be loaded yet */ }
|
|
54
|
+
// Recent commands from history
|
|
55
|
+
try {
|
|
56
|
+
const recent = getRecentHistory(20);
|
|
57
|
+
for (const entry of recent) {
|
|
58
|
+
if (entry.rawText)
|
|
59
|
+
set.add(entry.rawText);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { /* history may be empty */ }
|
|
63
|
+
cached = [...set].sort();
|
|
64
|
+
lastBuilt = Date.now();
|
|
65
|
+
return cached;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Readline-compatible completer function.
|
|
69
|
+
*
|
|
70
|
+
* Signature: (line: string) => [matches: string[], line: string]
|
|
71
|
+
*/
|
|
72
|
+
export function completeInput(line) {
|
|
73
|
+
if (Date.now() - lastBuilt > CACHE_TTL || cached.length === 0) {
|
|
74
|
+
buildCompletions();
|
|
75
|
+
}
|
|
76
|
+
const lower = line.toLowerCase();
|
|
77
|
+
const hits = cached.filter((c) => c.toLowerCase().startsWith(lower));
|
|
78
|
+
return [hits.length > 0 ? hits : cached, line];
|
|
79
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { RulesConfig } from "../types/rules.js";
|
|
4
4
|
import { IntentsConfig } from "../types/intent.js";
|
|
5
|
-
import { CONFIG_DIR } from "./paths.js";
|
|
5
|
+
import { CONFIG_DIR, USER_HOME } from "./paths.js";
|
|
6
6
|
import { pluginRegistry } from "../plugins/registry.js";
|
|
7
7
|
let cachedRules = null;
|
|
8
8
|
let cachedIntents = null;
|
|
@@ -40,6 +40,36 @@ export function loadIntents(forceReload = false) {
|
|
|
40
40
|
cachedIntents.intents.push(pi);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
// Merge user custom intents from ~/.notoken/custom-intents.json
|
|
44
|
+
const customFile = resolve(USER_HOME, "custom-intents.json");
|
|
45
|
+
if (existsSync(customFile)) {
|
|
46
|
+
try {
|
|
47
|
+
const customRaw = readFileSync(customFile, "utf-8");
|
|
48
|
+
const customData = JSON.parse(customRaw);
|
|
49
|
+
const customIntents = customData.intents ?? [];
|
|
50
|
+
for (const ci of customIntents) {
|
|
51
|
+
const entry = ci;
|
|
52
|
+
// Build a full IntentDef with sensible defaults for user-defined intents
|
|
53
|
+
const def = {
|
|
54
|
+
name: entry.name,
|
|
55
|
+
description: entry.description ?? "",
|
|
56
|
+
synonyms: entry.synonyms ?? [],
|
|
57
|
+
fields: entry.fields ?? {},
|
|
58
|
+
command: entry.command ?? "",
|
|
59
|
+
execution: entry.execution ?? "local",
|
|
60
|
+
requiresConfirmation: entry.requiresConfirmation ?? true,
|
|
61
|
+
riskLevel: entry.riskLevel ?? "medium",
|
|
62
|
+
examples: entry.examples ?? entry.synonyms ?? [],
|
|
63
|
+
};
|
|
64
|
+
if (!cachedIntents.intents.find((i) => i.name === def.name)) {
|
|
65
|
+
cachedIntents.intents.push(def);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Silently ignore malformed custom intents file
|
|
71
|
+
}
|
|
72
|
+
}
|
|
43
73
|
return cachedIntents.intents;
|
|
44
74
|
}
|
|
45
75
|
export function getIntentDef(name) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Natural language → SQL query builder.
|
|
3
|
+
*
|
|
4
|
+
* Translates phrases like:
|
|
5
|
+
* "look at items and find sally" → SELECT * FROM items WHERE ... LIKE '%sally%'
|
|
6
|
+
* "show me all users" → SELECT * FROM users
|
|
7
|
+
* "count orders" → SELECT COUNT(*) FROM orders
|
|
8
|
+
* "show tables" → \dt (postgres) or SHOW TABLES (mysql)
|
|
9
|
+
* "describe users table" → \d users or DESCRIBE users
|
|
10
|
+
* "find orders where total > 100" → SELECT * FROM orders WHERE total > 100
|
|
11
|
+
*/
|
|
12
|
+
export type DbType = "postgres" | "mysql";
|
|
13
|
+
export interface DbQueryResult {
|
|
14
|
+
query: string;
|
|
15
|
+
command: string;
|
|
16
|
+
explanation: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build a SQL query from natural language.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildQuery(rawText: string, fields: Record<string, unknown>, dbType?: DbType): DbQueryResult;
|
|
22
|
+
/**
|
|
23
|
+
* Format query result for display — show the SQL, explain it, then run.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatQueryPlan(result: DbQueryResult): string;
|