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.
Files changed (99) hide show
  1. package/config/chat-responses.json +767 -0
  2. package/config/concept-clusters.json +31 -0
  3. package/config/entities.json +93 -0
  4. package/config/image-prompts.json +20 -0
  5. package/config/intent-vectors.json +1 -0
  6. package/config/intents.json +4946 -83
  7. package/config/ollama-models.json +193 -0
  8. package/config/rules.json +32 -1
  9. package/dist/automation/discordPatchright.d.ts +35 -0
  10. package/dist/automation/discordPatchright.js +424 -0
  11. package/dist/automation/discordSetup.d.ts +31 -0
  12. package/dist/automation/discordSetup.js +338 -0
  13. package/dist/conversation/coreference.js +44 -4
  14. package/dist/conversation/pendingActions.d.ts +55 -0
  15. package/dist/conversation/pendingActions.js +127 -0
  16. package/dist/conversation/store.d.ts +72 -0
  17. package/dist/conversation/store.js +140 -1
  18. package/dist/conversation/topicTracker.d.ts +36 -0
  19. package/dist/conversation/topicTracker.js +141 -0
  20. package/dist/execution/ssh.d.ts +42 -1
  21. package/dist/execution/ssh.js +532 -3
  22. package/dist/handlers/executor.js +3981 -16
  23. package/dist/index.d.ts +25 -3
  24. package/dist/index.js +36 -2
  25. package/dist/nlp/batchParser.d.ts +30 -0
  26. package/dist/nlp/batchParser.js +77 -0
  27. package/dist/nlp/conceptExpansion.d.ts +54 -0
  28. package/dist/nlp/conceptExpansion.js +136 -0
  29. package/dist/nlp/conceptRouter.d.ts +49 -0
  30. package/dist/nlp/conceptRouter.js +302 -0
  31. package/dist/nlp/confidenceCalibrator.d.ts +62 -0
  32. package/dist/nlp/confidenceCalibrator.js +116 -0
  33. package/dist/nlp/correctionLearner.d.ts +45 -0
  34. package/dist/nlp/correctionLearner.js +207 -0
  35. package/dist/nlp/entitySpellCorrect.d.ts +35 -0
  36. package/dist/nlp/entitySpellCorrect.js +141 -0
  37. package/dist/nlp/knowledgeGraph.d.ts +70 -0
  38. package/dist/nlp/knowledgeGraph.js +380 -0
  39. package/dist/nlp/llmFallback.js +28 -1
  40. package/dist/nlp/multiClassifier.js +91 -6
  41. package/dist/nlp/multiIntent.d.ts +43 -0
  42. package/dist/nlp/multiIntent.js +154 -0
  43. package/dist/nlp/parseIntent.d.ts +6 -1
  44. package/dist/nlp/parseIntent.js +180 -5
  45. package/dist/nlp/ruleParser.js +315 -0
  46. package/dist/nlp/semanticSimilarity.d.ts +30 -0
  47. package/dist/nlp/semanticSimilarity.js +174 -0
  48. package/dist/nlp/vocabularyBuilder.d.ts +43 -0
  49. package/dist/nlp/vocabularyBuilder.js +224 -0
  50. package/dist/nlp/wikidata.d.ts +49 -0
  51. package/dist/nlp/wikidata.js +228 -0
  52. package/dist/policy/confirm.d.ts +10 -0
  53. package/dist/policy/confirm.js +39 -0
  54. package/dist/policy/safety.js +6 -4
  55. package/dist/utils/aliases.d.ts +5 -0
  56. package/dist/utils/aliases.js +39 -0
  57. package/dist/utils/analysis.js +71 -15
  58. package/dist/utils/browser.d.ts +64 -0
  59. package/dist/utils/browser.js +364 -0
  60. package/dist/utils/commandHistory.d.ts +20 -0
  61. package/dist/utils/commandHistory.js +108 -0
  62. package/dist/utils/completer.d.ts +17 -0
  63. package/dist/utils/completer.js +79 -0
  64. package/dist/utils/config.js +32 -2
  65. package/dist/utils/dbQuery.d.ts +25 -0
  66. package/dist/utils/dbQuery.js +248 -0
  67. package/dist/utils/discordDiag.d.ts +35 -0
  68. package/dist/utils/discordDiag.js +826 -0
  69. package/dist/utils/diskCleanup.d.ts +36 -0
  70. package/dist/utils/diskCleanup.js +775 -0
  71. package/dist/utils/entityResolver.d.ts +107 -0
  72. package/dist/utils/entityResolver.js +468 -0
  73. package/dist/utils/imageGen.d.ts +92 -0
  74. package/dist/utils/imageGen.js +2031 -0
  75. package/dist/utils/installTracker.d.ts +57 -0
  76. package/dist/utils/installTracker.js +160 -0
  77. package/dist/utils/multiExec.d.ts +21 -0
  78. package/dist/utils/multiExec.js +141 -0
  79. package/dist/utils/openclawDiag.d.ts +29 -0
  80. package/dist/utils/openclawDiag.js +1035 -0
  81. package/dist/utils/output.js +4 -0
  82. package/dist/utils/platform.js +2 -1
  83. package/dist/utils/progressReporter.d.ts +50 -0
  84. package/dist/utils/progressReporter.js +58 -0
  85. package/dist/utils/projectDetect.d.ts +44 -0
  86. package/dist/utils/projectDetect.js +319 -0
  87. package/dist/utils/projectScanner.d.ts +44 -0
  88. package/dist/utils/projectScanner.js +312 -0
  89. package/dist/utils/shellCompat.d.ts +78 -0
  90. package/dist/utils/shellCompat.js +186 -0
  91. package/dist/utils/smartArchive.d.ts +16 -0
  92. package/dist/utils/smartArchive.js +172 -0
  93. package/dist/utils/smartRetry.d.ts +26 -0
  94. package/dist/utils/smartRetry.js +114 -0
  95. package/dist/utils/updater.d.ts +1 -0
  96. package/dist/utils/updater.js +1 -1
  97. package/dist/utils/version.d.ts +20 -0
  98. package/dist/utils/version.js +212 -0
  99. package/package.json +6 -3
@@ -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
- if (p.usePercent >= 95) {
109
- lines.push(` ${c.red}⚠ CRITICAL: ${p.mountPoint} is ${p.usePercent}% full (${p.available} free)${c.reset}`);
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 >= 85) {
113
- lines.push(` ${c.yellow}⚠ WARNING: ${p.mountPoint} is ${p.usePercent}% full (${p.available} free)${c.reset}`);
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 >= 70) {
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
- lines.push(` ${c.dim} Tip: Run "disk analysis" playbook for detailed breakdown.${c.reset}`);
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
- if (p.usePercent >= 95)
203
- return `${c.red}⚠ CRITICAL: ${p.usePercent}% full! Only ${p.available} free on ${p.size} total.${c.reset}`;
204
- if (p.usePercent >= 85)
205
- return `${c.yellow}⚠ WARNING: ${p.usePercent}% full. ${p.available} free on ${p.size} total.${c.reset}`;
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[];