notoken-core 1.6.0 → 2.0.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/chat-responses.json +767 -0
- package/config/concept-clusters.json +31 -0
- package/config/entities.json +93 -0
- package/config/image-prompts.json +20 -0
- package/config/intent-vectors.json +1 -0
- package/config/intents.json +4946 -83
- package/config/ollama-models.json +193 -0
- package/config/rules.json +32 -1
- 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/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 +532 -3
- package/dist/handlers/executor.js +3981 -16
- package/dist/index.d.ts +25 -3
- package/dist/index.js +36 -2
- 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 +315 -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/utils/aliases.d.ts +5 -0
- package/dist/utils/aliases.js +39 -0
- package/dist/utils/analysis.js +71 -15
- 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/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/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
package/dist/utils/analysis.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* - Directory: detects project types, file breakdowns
|
|
9
9
|
*/
|
|
10
10
|
import { analyzeDirectory as analyzeDirectoryImpl } from "./dirAnalysis.js";
|
|
11
|
+
import { detectLocalPlatform } from "./platform.js";
|
|
11
12
|
function analyzeDirectoryOutput(output) {
|
|
12
13
|
return analyzeDirectoryImpl(output);
|
|
13
14
|
}
|
|
@@ -74,6 +75,29 @@ export function analyzeLoad(output) {
|
|
|
74
75
|
else {
|
|
75
76
|
lines.push(` ${c.dim}→ Load is stable.${c.reset}`);
|
|
76
77
|
}
|
|
78
|
+
// Extract top CPU-heavy processes from the output
|
|
79
|
+
const psLines = output.split("\n").filter(l => /^\S+\s+\d+\s+\d+/.test(l));
|
|
80
|
+
const heavyProcs = psLines
|
|
81
|
+
.map(l => {
|
|
82
|
+
const parts = l.trim().split(/\s+/);
|
|
83
|
+
const cpu = parseFloat(parts[2]);
|
|
84
|
+
const mem = parseFloat(parts[3]);
|
|
85
|
+
const cmd = parts.slice(10).join(" ").replace(/^\/\S+\//, "").split(" ")[0]; // basename
|
|
86
|
+
return { user: parts[0], pid: parts[1], cpu, mem, cmd };
|
|
87
|
+
})
|
|
88
|
+
.filter(p => p.cpu > 5) // Only show processes using >5% CPU
|
|
89
|
+
.filter((p, i, arr) => arr.findIndex(x => x.pid === p.pid) === i) // Dedup by PID
|
|
90
|
+
.slice(0, 5);
|
|
91
|
+
if (heavyProcs.length > 0) {
|
|
92
|
+
lines.push(`\n ${c.bold}Heavy processes:${c.reset}`);
|
|
93
|
+
for (const p of heavyProcs) {
|
|
94
|
+
const cpuBar = p.cpu > 50 ? c.red : p.cpu > 20 ? c.yellow : c.dim;
|
|
95
|
+
lines.push(` ${cpuBar}${p.cpu.toFixed(0)}% CPU${c.reset} ${p.mem.toFixed(0)}% RAM ${c.bold}${p.cmd}${c.reset} ${c.dim}(${p.user}, PID ${p.pid})${c.reset}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (ratio1 < 0.3) {
|
|
99
|
+
lines.push(`\n ${c.green}No heavy processes — system is idle.${c.reset}`);
|
|
100
|
+
}
|
|
77
101
|
return lines.join("\n");
|
|
78
102
|
}
|
|
79
103
|
export function analyzeDisk(output, specificPath) {
|
|
@@ -103,17 +127,24 @@ export function analyzeDisk(output, specificPath) {
|
|
|
103
127
|
!p.filesystem.startsWith("none") &&
|
|
104
128
|
!p.filesystem.startsWith("rootfs") &&
|
|
105
129
|
p.mountPoint !== "/snap" &&
|
|
106
|
-
!p.mountPoint.startsWith("/snap/")
|
|
130
|
+
!p.mountPoint.startsWith("/snap/") &&
|
|
131
|
+
!p.mountPoint.includes("docker-desktop/cli-tools") &&
|
|
132
|
+
!p.filesystem.startsWith("/dev/loop"));
|
|
107
133
|
for (const p of realPartitions) {
|
|
108
|
-
|
|
109
|
-
|
|
134
|
+
// Use absolute free space (GB) for thresholds — percentage is misleading on large drives
|
|
135
|
+
// e.g. 97% on 2TB = 60GB free (fine), 95% on 100GB = 5GB free (critical)
|
|
136
|
+
const freeGB = parseFloat(p.available.replace(/[^\d.]/g, ""));
|
|
137
|
+
const freeUnit = p.available.replace(/[\d.]/g, "").trim().toUpperCase();
|
|
138
|
+
const freeGBNorm = freeUnit.startsWith("T") ? freeGB * 1024 : freeUnit.startsWith("M") ? freeGB / 1024 : freeGB;
|
|
139
|
+
if (freeGBNorm < 5) {
|
|
140
|
+
lines.push(` ${c.red}⚠ CRITICAL: ${p.mountPoint} has only ${p.available} free (${p.usePercent}% used)${c.reset}`);
|
|
110
141
|
criticalCount++;
|
|
111
142
|
}
|
|
112
|
-
else if (p.usePercent >=
|
|
113
|
-
lines.push(` ${c.yellow}⚠ WARNING: ${p.mountPoint}
|
|
143
|
+
else if (freeGBNorm < 20 && p.usePercent >= 90) {
|
|
144
|
+
lines.push(` ${c.yellow}⚠ WARNING: ${p.mountPoint} has ${p.available} free (${p.usePercent}% used)${c.reset}`);
|
|
114
145
|
warningCount++;
|
|
115
146
|
}
|
|
116
|
-
else if (p.usePercent >=
|
|
147
|
+
else if (p.usePercent >= 85) {
|
|
117
148
|
lines.push(` ${c.dim} ${p.mountPoint}: ${p.usePercent}% used (${p.available} free)${c.reset}`);
|
|
118
149
|
}
|
|
119
150
|
}
|
|
@@ -127,7 +158,29 @@ export function analyzeDisk(output, specificPath) {
|
|
|
127
158
|
if (warningCount > 0) {
|
|
128
159
|
lines.push(` ${c.yellow} ${warningCount} partition(s) approaching full.${c.reset}`);
|
|
129
160
|
}
|
|
130
|
-
|
|
161
|
+
if (criticalCount > 0) {
|
|
162
|
+
lines.push(` ${c.yellow}${c.bold} → Try "free up space" or "disk cleanup" to scan for reclaimable space.${c.reset}`);
|
|
163
|
+
// WSL-specific: warn about I/O errors and need to restart WSL
|
|
164
|
+
const platform = detectLocalPlatform();
|
|
165
|
+
if (platform.isWSL) {
|
|
166
|
+
const windowsDriveFull = realPartitions.some((p) => {
|
|
167
|
+
if (!p.mountPoint.startsWith("/mnt/"))
|
|
168
|
+
return false;
|
|
169
|
+
const fGB = parseFloat(p.available.replace(/[^\d.]/g, ""));
|
|
170
|
+
const fU = p.available.replace(/[\d.]/g, "").trim().toUpperCase();
|
|
171
|
+
const freeNorm = fU.startsWith("T") ? fGB * 1024 : fU.startsWith("M") ? fGB / 1024 : fGB;
|
|
172
|
+
return freeNorm < 5;
|
|
173
|
+
});
|
|
174
|
+
if (windowsDriveFull) {
|
|
175
|
+
lines.push(`\n ${c.red}${c.bold} ⚠ WSL WARNING:${c.reset} Windows drive is critically full.`);
|
|
176
|
+
lines.push(` ${c.yellow} WSL shares disk with Windows — I/O errors and instability are likely.${c.reset}`);
|
|
177
|
+
lines.push(` ${c.yellow} Run "free up space" to clean and optionally restart WSL.${c.reset}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
lines.push(` ${c.dim} Tip: Run "disk analysis" playbook for detailed breakdown.${c.reset}`);
|
|
183
|
+
}
|
|
131
184
|
}
|
|
132
185
|
// Highlight largest partitions
|
|
133
186
|
const sorted = [...realPartitions].sort((a, b) => b.usePercent - a.usePercent);
|
|
@@ -173,10 +226,10 @@ function findPartitionForPath(partitions, path) {
|
|
|
173
226
|
"var": ["/var"],
|
|
174
227
|
"log": ["/var/log", "/var"],
|
|
175
228
|
"www": ["/var/www"],
|
|
176
|
-
"c drive": ["/mnt/c"],
|
|
177
|
-
"d drive": ["/mnt/d"],
|
|
178
|
-
"e drive": ["/mnt/e"],
|
|
179
|
-
"f drive": ["/mnt/f"],
|
|
229
|
+
"c drive": ["/mnt/c", "C:\\"],
|
|
230
|
+
"d drive": ["/mnt/d", "D:\\"],
|
|
231
|
+
"e drive": ["/mnt/e", "E:\\"],
|
|
232
|
+
"f drive": ["/mnt/f", "F:\\"],
|
|
180
233
|
};
|
|
181
234
|
// Check aliases first
|
|
182
235
|
for (const [alias, paths] of Object.entries(aliases)) {
|
|
@@ -199,10 +252,13 @@ function findPartitionForPath(partitions, path) {
|
|
|
199
252
|
return null;
|
|
200
253
|
}
|
|
201
254
|
function formatPartitionHealth(p) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
255
|
+
const fGB = parseFloat(p.available.replace(/[^\d.]/g, ""));
|
|
256
|
+
const fU = p.available.replace(/[\d.]/g, "").trim().toUpperCase();
|
|
257
|
+
const freeNorm = fU.startsWith("T") ? fGB * 1024 : fU.startsWith("M") ? fGB / 1024 : fGB;
|
|
258
|
+
if (freeNorm < 5)
|
|
259
|
+
return `${c.red}⚠ CRITICAL: Only ${p.available} free on ${p.size} total (${p.usePercent}% used).${c.reset}`;
|
|
260
|
+
if (freeNorm < 20 && p.usePercent >= 90)
|
|
261
|
+
return `${c.yellow}⚠ WARNING: ${p.available} free on ${p.size} total (${p.usePercent}% used).${c.reset}`;
|
|
206
262
|
if (p.usePercent >= 70)
|
|
207
263
|
return `${c.dim}Moderate: ${p.usePercent}% full. ${p.available} free on ${p.size} total.${c.reset}`;
|
|
208
264
|
return `${c.green}✓ Healthy: ${p.usePercent}% used. ${p.available} free on ${p.size} total.${c.reset}`;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
export type BrowserEngine = "patchright" | "playwright" | "docker" | "system";
|
|
21
|
+
export interface BrowserStatus {
|
|
22
|
+
engine: BrowserEngine;
|
|
23
|
+
available: boolean;
|
|
24
|
+
version?: string;
|
|
25
|
+
browsersInstalled?: boolean;
|
|
26
|
+
dockerImage?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface BrowseOptions {
|
|
29
|
+
url: string;
|
|
30
|
+
headless?: boolean;
|
|
31
|
+
screenshot?: boolean;
|
|
32
|
+
screenshotPath?: string;
|
|
33
|
+
waitFor?: number;
|
|
34
|
+
userAgent?: string;
|
|
35
|
+
viewport?: {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface BrowseResult {
|
|
41
|
+
engine: BrowserEngine;
|
|
42
|
+
url: string;
|
|
43
|
+
title?: string;
|
|
44
|
+
screenshotPath?: string;
|
|
45
|
+
error?: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function detectBrowserEngines(): BrowserStatus[];
|
|
48
|
+
/**
|
|
49
|
+
* Get the best available engine.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getBestEngine(): BrowserStatus | null;
|
|
52
|
+
export interface InstallResult {
|
|
53
|
+
success: boolean;
|
|
54
|
+
engine: BrowserEngine;
|
|
55
|
+
message: string;
|
|
56
|
+
}
|
|
57
|
+
export declare function installBrowserEngine(engine?: BrowserEngine): Promise<InstallResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Open a URL using the best available engine.
|
|
60
|
+
*/
|
|
61
|
+
export declare function browse(opts: BrowseOptions): Promise<BrowseResult>;
|
|
62
|
+
export declare function normalizeUrl(url: string): string;
|
|
63
|
+
export declare function formatBrowserStatus(engines: BrowserStatus[]): string;
|
|
64
|
+
export declare function stopDockerBrowser(): string;
|
|
@@ -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[];
|