notoken-core 1.8.0 → 1.8.1

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.
@@ -219,14 +219,16 @@ const USER_DATA_DIR = 'C:\\\\temp\\\\notoken-browser-profile';
219
219
  console.log('TOKEN_NOT_FOUND');
220
220
  }
221
221
 
222
- // Enable intents
222
+ // Enable privileged intents (but NOT Code Grant which is switch index 1)
223
223
  await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
224
224
  await page.waitForTimeout(1500);
225
225
  await page.locator('text=Privileged Gateway Intents').scrollIntoViewIfNeeded().catch(() => {});
226
226
  await page.waitForTimeout(1000);
227
- const switches = await page.locator('label[data-react-aria-pressable="true"] input[role="switch"]').all();
227
+ const allSwitches = await page.locator('input[role="switch"]').all();
228
228
  let toggled = 0;
229
- for (const sw of switches) {
229
+ // Skip switches 0 (Public Bot) and 1 (Code Grant) — only toggle intent switches (index 2+)
230
+ for (let idx = 2; idx < allSwitches.length; idx++) {
231
+ const sw = allSwitches[idx];
230
232
  if (!await sw.isChecked().catch(() => true)) {
231
233
  await sw.locator('..').locator('..').first().click({ force: true }).catch(() => {});
232
234
  await page.waitForTimeout(500);
@@ -236,6 +238,17 @@ const USER_DATA_DIR = 'C:\\\\temp\\\\notoken-browser-profile';
236
238
  await page.click('button:has-text("Save Changes")', { timeout: 3000 }).catch(() => {});
237
239
  console.log('INTENTS_ENABLED:' + toggled);
238
240
 
241
+ // Ensure Code Grant is OFF (switch index 1)
242
+ if (allSwitches.length >= 2) {
243
+ const codeGrantOn = await allSwitches[1].isChecked().catch(() => false);
244
+ if (codeGrantOn) {
245
+ await allSwitches[1].locator('..').click({ force: true }).catch(() => {});
246
+ await page.waitForTimeout(500);
247
+ await page.click('button:has-text("Save Changes")', { timeout: 3000 }).catch(() => {});
248
+ console.log('CODE_GRANT_DISABLED');
249
+ }
250
+ }
251
+
239
252
  // Save result
240
253
  const result = { token, appId, success: !!token };
241
254
  fs.writeFileSync('C:\\\\temp\\\\discord-bot-result.json', JSON.stringify(result));
@@ -373,10 +373,83 @@ export async function executeIntent(intent) {
373
373
  return `\x1b[31m✗ Discord diagnostics error: ${err.message.split("\n")[0]}\x1b[0m`;
374
374
  }
375
375
  }
376
- // OpenClaw status
376
+ // OpenClaw status — quick summary or detailed
377
377
  if (intent.intent === "openclaw.status") {
378
+ const isDetailed = /\b(details?|detailed|verbose|full|deep|all|everything)\b/i.test(intent.rawText);
378
379
  const diagRemote = environment !== "local" && environment !== "localhost" && hasRealHost(environment);
379
- return diagRemote ? await quickConnectivityCheck((cmd) => runRemoteCommand(environment, cmd)) : await quickConnectivityCheck();
380
+ if (isDetailed) {
381
+ // Full detailed check (existing behavior)
382
+ return diagRemote ? await quickConnectivityCheck((cmd) => runRemoteCommand(environment, cmd)) : await quickConnectivityCheck();
383
+ }
384
+ // Quick summary — fast, one-line-per-item
385
+ const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m" };
386
+ const lines = [];
387
+ lines.push(`\n${cc.bold}${cc.cyan}── OpenClaw ──${cc.reset}\n`);
388
+ // Gateway running?
389
+ const health = await runLocalCommand("curl -sf http://127.0.0.1:18789/health 2>/dev/null").catch(() => "");
390
+ const gwUp = health.includes('"ok"');
391
+ lines.push(` ${gwUp ? cc.green + "✓" : cc.red + "✗"}${cc.reset} Gateway: ${gwUp ? "running" : "not running"}`);
392
+ // Environment
393
+ const { getUserContext } = await import("../utils/userContext.js");
394
+ const ctx = getUserContext();
395
+ lines.push(` ${cc.bold}Env:${cc.reset} ${ctx.isWSL ? "WSL" : ctx.isWindows ? "Windows" : "Linux"} (${ctx.effectiveUser})`);
396
+ // Current model
397
+ const ocConfig = await runLocalCommand("cat /root/.openclaw/openclaw.json 2>/dev/null || cat ~/.openclaw/openclaw.json 2>/dev/null").catch(() => "");
398
+ const modelMatch = ocConfig.match(/"primary"\s*:\s*"([^"]+)"/);
399
+ if (modelMatch)
400
+ lines.push(` ${cc.bold}Model:${cc.reset} ${modelMatch[1]}`);
401
+ // LLM providers — quick check auth profiles
402
+ try {
403
+ const { readFileSync, existsSync } = await import("node:fs");
404
+ const { getAuthProfilesPath } = await import("../utils/userContext.js");
405
+ const authPath = getAuthProfilesPath();
406
+ if (existsSync(authPath)) {
407
+ const auth = JSON.parse(readFileSync(authPath, "utf-8"));
408
+ const providers = [];
409
+ for (const [name, profile] of Object.entries(auth.profiles ?? {})) {
410
+ const expires = profile.expires ?? 0;
411
+ const hoursLeft = (expires - Date.now()) / 3600000;
412
+ const pName = name.split(":")[0];
413
+ if (hoursLeft > 0) {
414
+ providers.push(`${cc.green}✓${cc.reset} ${pName} (${Math.round(hoursLeft)}h)`);
415
+ }
416
+ else if (profile.type === "token" || profile.type === "api_key") {
417
+ providers.push(`${cc.green}✓${cc.reset} ${pName} (static)`);
418
+ }
419
+ else if (expires > 0) {
420
+ providers.push(`${cc.red}✗${cc.reset} ${pName} (expired)`);
421
+ }
422
+ }
423
+ if (providers.length > 0) {
424
+ lines.push(` ${cc.bold}LLMs:${cc.reset} ${providers.join(" ")}`);
425
+ }
426
+ }
427
+ }
428
+ catch { }
429
+ // Channels — check Discord
430
+ const discordToken = ocConfig.includes('"discord"') && ocConfig.includes('"token"');
431
+ if (discordToken) {
432
+ // Quick check if Discord is connected via logs
433
+ const recentLog = await runLocalCommand("tail -20 /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log 2>/dev/null").catch(() => "");
434
+ const discordOk = recentLog.includes("logged in to discord") || recentLog.includes("discord client ready");
435
+ const discordAwaiting = recentLog.includes("awaiting gateway readiness");
436
+ if (discordOk) {
437
+ lines.push(` ${cc.green}✓${cc.reset} Discord: connected`);
438
+ }
439
+ else if (discordAwaiting) {
440
+ lines.push(` ${cc.yellow}⏳${cc.reset} Discord: connecting...`);
441
+ }
442
+ else {
443
+ lines.push(` ${cc.yellow}○${cc.reset} Discord: configured`);
444
+ }
445
+ }
446
+ // Ollama
447
+ const ollamaUp = await runLocalCommand("curl -sf http://localhost:11434/api/tags 2>/dev/null | head -1").catch(() => "");
448
+ if (ollamaUp.includes("models")) {
449
+ lines.push(` ${cc.green}✓${cc.reset} Ollama: running`);
450
+ }
451
+ lines.push(`\n ${cc.dim}Say "openclaw status details" for full diagnostics${cc.reset}`);
452
+ return lines.join("\n");
380
453
  }
381
454
  // OpenClaw diagnose
382
455
  if (intent.intent === "openclaw.diagnose") {
@@ -1249,6 +1322,22 @@ expect eof
1249
1322
  return getRandomTip();
1250
1323
  }
1251
1324
  // Notoken status — comprehensive overview
1325
+ // notoken.versions — show current version and check for updates
1326
+ if (intent.intent === "notoken.versions" || intent.intent === "notoken.version") {
1327
+ try {
1328
+ const { checkForUpdate } = await import("../utils/updater.js");
1329
+ const info = await checkForUpdate();
1330
+ if (!info)
1331
+ return `\x1b[36mNoToken\x1b[0m v1.8.0\n\x1b[2mCould not check for updates.\x1b[0m`;
1332
+ if (info.updateAvailable) {
1333
+ return `\x1b[36mNoToken\x1b[0m v${info.current}\n\x1b[33m⬆ Update available: ${info.current} → ${info.latest}\x1b[0m\n\x1b[2m Run: notoken update or /update\x1b[0m`;
1334
+ }
1335
+ return `\x1b[36mNoToken\x1b[0m v${info.current}\n\x1b[32m✓ You're on the latest version.\x1b[0m`;
1336
+ }
1337
+ catch {
1338
+ return `\x1b[36mNoToken\x1b[0m v1.8.0`;
1339
+ }
1340
+ }
1252
1341
  // notoken.jobs — in one-shot mode just say "use interactive mode"
1253
1342
  if (intent.intent === "notoken.jobs") {
1254
1343
  return `\x1b[32m✓\x1b[0m No background tasks (one-shot mode).\n\x1b[2m Run \x1b[1mnotoken\x1b[0m\x1b[2m for interactive mode with background task support.\x1b[0m`;
@@ -1397,6 +1486,178 @@ expect eof
1397
1486
  }
1398
1487
  }
1399
1488
  }
1489
+ // ── Ollama environment detection — WSL vs Windows host ──
1490
+ // Detects where Ollama is installed and routes commands to the right side.
1491
+ // User can say "on windows" or "on wsl" to override.
1492
+ /**
1493
+ * Run an Ollama command on the right environment.
1494
+ * Detects: WSL local, Windows host, Docker container.
1495
+ * User can override with "on windows" / "on wsl" / "in docker".
1496
+ */
1497
+ async function runOllama(cmd) {
1498
+ const isWSL = (await runLocalCommand("grep -qi microsoft /proc/version 2>/dev/null && echo wsl || echo native").catch(() => "native")).trim() === "wsl";
1499
+ const raw = intent.rawText.toLowerCase();
1500
+ // Explicit target from user
1501
+ const forceWindows = /\b(on\s+)?windows\b|\bhost\b/i.test(raw);
1502
+ const forceWSL = /\b(on\s+|in\s+)?wsl\b|\blinux\b/i.test(raw);
1503
+ const forceDocker = /\b(in\s+)?docker\b|\bcontainer\b/i.test(raw);
1504
+ // Default: run on current environment (where notoken is running)
1505
+ // Only check other environments if user explicitly asks or local fails
1506
+ const catchErr = (e) => { const err = e; return err.stdout ?? err.stderr ?? err.message ?? "failed"; };
1507
+ // Explicit overrides first
1508
+ if (forceDocker) {
1509
+ const container = (await runLocalCommand("docker ps --format '{{.Names}}' 2>/dev/null | grep -i ollama").catch(() => "")).trim().split("\n")[0];
1510
+ if (container) {
1511
+ console.log(`\x1b[2m[Ollama in Docker: ${container}]\x1b[0m`);
1512
+ return runLocalCommand(`docker exec ${container} ollama ${cmd} 2>&1`, 300_000).catch(catchErr);
1513
+ }
1514
+ return "No Ollama Docker container found. Start one: docker run -d --name ollama ollama/ollama";
1515
+ }
1516
+ if (forceWindows && isWSL) {
1517
+ console.log(`\x1b[2m[Ollama on Windows host]\x1b[0m`);
1518
+ return runLocalCommand(`cmd.exe /c 'ollama ${cmd}' 2>&1`, 300_000).catch(catchErr);
1519
+ }
1520
+ if (forceWSL || !isWSL) {
1521
+ // Current env is WSL or native Linux — run directly
1522
+ return runLocalCommand(`ollama ${cmd} 2>&1`, 300_000).catch(catchErr);
1523
+ }
1524
+ // Default for WSL: try local first, then Windows host
1525
+ const localResult = await runLocalCommand(`ollama ${cmd} 2>&1`, 300_000).catch(() => "");
1526
+ if (localResult && !localResult.includes("not found") && !localResult.includes("command not found")) {
1527
+ return localResult;
1528
+ }
1529
+ // Local failed — try Windows host silently
1530
+ const winResult = await runLocalCommand(`cmd.exe /c 'ollama ${cmd}' 2>&1`, 300_000).catch(() => "");
1531
+ if (winResult && !winResult.includes("not recognized")) {
1532
+ console.log(`\x1b[2m[Using Windows host Ollama]\x1b[0m`);
1533
+ return winResult;
1534
+ }
1535
+ return "Ollama not found. Install: curl -fsSL https://ollama.com/install.sh | sh";
1536
+ }
1537
+ // Ollama status — quick check if running + version
1538
+ if (intent.intent === "ollama.status") {
1539
+ const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m" };
1540
+ const lines = [];
1541
+ lines.push(`\n${cc.bold}${cc.cyan}── Ollama ──${cc.reset}\n`);
1542
+ // Check if installed
1543
+ const version = await runLocalCommand("ollama --version 2>/dev/null").catch(() => "");
1544
+ if (!version) {
1545
+ lines.push(` ${cc.red}✗${cc.reset} Ollama not installed`);
1546
+ lines.push(` ${cc.dim}Install: curl -fsSL https://ollama.com/install.sh | sh${cc.reset}`);
1547
+ return lines.join("\n");
1548
+ }
1549
+ lines.push(` ${cc.green}✓${cc.reset} Installed: ${version.trim()}`);
1550
+ // Check if running
1551
+ const tags = await runLocalCommand("curl -sf http://127.0.0.1:11434/api/tags 2>/dev/null").catch(() => "");
1552
+ if (tags.includes("models")) {
1553
+ const models = JSON.parse(tags).models ?? [];
1554
+ lines.push(` ${cc.green}✓${cc.reset} Running on port 11434`);
1555
+ lines.push(` ${cc.bold}Models:${cc.reset} ${models.length} installed`);
1556
+ for (const m of models.slice(0, 5)) {
1557
+ const size = (m.size / 1024 / 1024 / 1024).toFixed(1);
1558
+ lines.push(` ${cc.green}•${cc.reset} ${m.name} (${size}GB)`);
1559
+ }
1560
+ }
1561
+ else {
1562
+ lines.push(` ${cc.yellow}○${cc.reset} Not running`);
1563
+ lines.push(` ${cc.dim}Start: "start ollama"${cc.reset}`);
1564
+ }
1565
+ // GPU
1566
+ const gpu = await runLocalCommand("nvidia-smi --query-gpu=name,memory.total --format=csv,noheader,nounits 2>/dev/null").catch(() => "");
1567
+ if (gpu.trim()) {
1568
+ const [name, mem] = gpu.trim().split(",").map(s => s.trim());
1569
+ lines.push(` ${cc.bold}GPU:${cc.reset} ${name} (${mem}MB VRAM)`);
1570
+ }
1571
+ lines.push(`\n ${cc.dim}Say: "ollama models" for details, "ollama pull llama3.2" to download${cc.reset}`);
1572
+ return lines.join("\n");
1573
+ }
1574
+ // Ollama uninstall — platform-aware removal
1575
+ if (intent.intent === "ollama.uninstall") {
1576
+ const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m" };
1577
+ const isWSL = (await runLocalCommand("grep -qi microsoft /proc/version 2>/dev/null && echo wsl || echo native").catch(() => "native")).trim() === "wsl";
1578
+ const isWin = process.platform === "win32";
1579
+ const isMac = process.platform === "darwin";
1580
+ const removeModels = /\b(completely|everything|and models|all data)\b/i.test(intent.rawText);
1581
+ const lines = [];
1582
+ lines.push(`\n${cc.bold}${cc.cyan}── Uninstall Ollama ──${cc.reset}\n`);
1583
+ // Check what's installed
1584
+ const version = await runLocalCommand("ollama --version 2>/dev/null").catch(() => "");
1585
+ const models = await runLocalCommand("curl -sf http://127.0.0.1:11434/api/tags 2>/dev/null").catch(() => "");
1586
+ let modelCount = 0;
1587
+ let modelSize = "unknown";
1588
+ try {
1589
+ const parsed = JSON.parse(models);
1590
+ modelCount = parsed.models?.length ?? 0;
1591
+ const totalBytes = parsed.models?.reduce((sum, m) => sum + (m.size ?? 0), 0) ?? 0;
1592
+ modelSize = (totalBytes / 1024 / 1024 / 1024).toFixed(1) + "GB";
1593
+ }
1594
+ catch { }
1595
+ if (!version) {
1596
+ return `${cc.dim}Ollama doesn't appear to be installed.${cc.reset}`;
1597
+ }
1598
+ lines.push(` ${cc.bold}Installed:${cc.reset} ${version.trim()}`);
1599
+ if (modelCount > 0)
1600
+ lines.push(` ${cc.bold}Models:${cc.reset} ${modelCount} installed (${modelSize})`);
1601
+ // Stop the service first
1602
+ lines.push(`\n ${cc.dim}Stopping Ollama service...${cc.reset}`);
1603
+ await runLocalCommand("systemctl stop ollama 2>/dev/null").catch(() => "");
1604
+ await runLocalCommand("pkill -f ollama 2>/dev/null").catch(() => "");
1605
+ if (isWSL || (!isWin && !isMac)) {
1606
+ // Linux / WSL
1607
+ lines.push(` ${cc.bold}Platform:${cc.reset} ${isWSL ? "WSL" : "Linux"}`);
1608
+ // Remove binary
1609
+ await runLocalCommand("sudo rm -f /usr/local/bin/ollama 2>/dev/null").catch(() => "");
1610
+ lines.push(` ${cc.green}✓${cc.reset} Removed /usr/local/bin/ollama`);
1611
+ // Remove systemd service
1612
+ await runLocalCommand("sudo systemctl disable ollama 2>/dev/null").catch(() => "");
1613
+ await runLocalCommand("sudo rm -f /etc/systemd/system/ollama.service 2>/dev/null").catch(() => "");
1614
+ await runLocalCommand("sudo systemctl daemon-reload 2>/dev/null").catch(() => "");
1615
+ lines.push(` ${cc.green}✓${cc.reset} Removed systemd service`);
1616
+ // Remove user
1617
+ await runLocalCommand("sudo userdel -r ollama 2>/dev/null").catch(() => "");
1618
+ await runLocalCommand("sudo groupdel ollama 2>/dev/null").catch(() => "");
1619
+ lines.push(` ${cc.green}✓${cc.reset} Removed ollama user/group`);
1620
+ // Remove models if requested
1621
+ if (removeModels) {
1622
+ const modelDirs = ["/usr/share/ollama", `${process.env.HOME}/.ollama`, process.env.OLLAMA_MODELS].filter(Boolean);
1623
+ for (const dir of modelDirs) {
1624
+ if (dir) {
1625
+ await runLocalCommand(`sudo rm -rf "${dir}" 2>/dev/null`).catch(() => "");
1626
+ lines.push(` ${cc.green}✓${cc.reset} Removed ${dir}`);
1627
+ }
1628
+ }
1629
+ }
1630
+ else {
1631
+ lines.push(`\n ${cc.yellow}⚠${cc.reset} Models kept. To remove: "uninstall ollama completely"`);
1632
+ const modelDir = process.env.OLLAMA_MODELS ?? "/usr/share/ollama/.ollama/models";
1633
+ lines.push(` ${cc.dim}Model directory: ${modelDir}${cc.reset}`);
1634
+ }
1635
+ // Check Windows side too if in WSL
1636
+ if (isWSL) {
1637
+ const winOllama = await runLocalCommand("cmd.exe /c 'where ollama' 2>/dev/null").catch(() => "");
1638
+ if (winOllama.includes("ollama")) {
1639
+ lines.push(`\n ${cc.yellow}⚠${cc.reset} Ollama also installed on Windows host.`);
1640
+ lines.push(` ${cc.dim}To remove from Windows: Settings → Apps → Ollama → Uninstall${cc.reset}`);
1641
+ }
1642
+ }
1643
+ }
1644
+ else if (isMac) {
1645
+ lines.push(` ${cc.bold}Platform:${cc.reset} macOS`);
1646
+ await runLocalCommand("brew uninstall ollama 2>/dev/null || rm -f /usr/local/bin/ollama").catch(() => "");
1647
+ lines.push(` ${cc.green}✓${cc.reset} Removed Ollama`);
1648
+ if (removeModels) {
1649
+ await runLocalCommand(`rm -rf ${process.env.HOME}/.ollama 2>/dev/null`).catch(() => "");
1650
+ lines.push(` ${cc.green}✓${cc.reset} Removed ~/.ollama`);
1651
+ }
1652
+ }
1653
+ else if (isWin) {
1654
+ lines.push(` ${cc.bold}Platform:${cc.reset} Windows`);
1655
+ lines.push(` ${cc.dim}Use Settings → Apps → Ollama → Uninstall${cc.reset}`);
1656
+ lines.push(` ${cc.dim}Or: winget uninstall ollama${cc.reset}`);
1657
+ }
1658
+ lines.push(`\n ${cc.green}✓${cc.reset} ${cc.bold}Ollama uninstalled.${cc.reset}`);
1659
+ return lines.join("\n");
1660
+ }
1400
1661
  // Ollama model management
1401
1662
  if (intent.intent === "ollama.models" || intent.intent === "ollama.list") {
1402
1663
  const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m" };
@@ -1405,8 +1666,8 @@ expect eof
1405
1666
  const totalRAMGB = Math.round(parseInt(ramOut) / 1073741824);
1406
1667
  const freeRamOut = await runLocalCommand("free -b | grep Mem | awk '{print $7}'").catch(() => "0");
1407
1668
  const freeRAMGB = Math.round(parseInt(freeRamOut) / 1073741824);
1408
- // Get installed models
1409
- const installed = await runLocalCommand("ollama list 2>&1").catch(() => "Ollama not running");
1669
+ // Get installed models — checks both WSL and Windows
1670
+ const installed = await runOllama("list").catch(() => "Ollama not running");
1410
1671
  const lines = [];
1411
1672
  lines.push(`\n${cc.bold}${cc.cyan}── Ollama Models ──${cc.reset}\n`);
1412
1673
  lines.push(` ${cc.bold}System:${cc.reset} ${totalRAMGB}GB RAM (${freeRAMGB}GB available)\n`);
@@ -1526,7 +1787,7 @@ expect eof
1526
1787
  }
1527
1788
  console.log(lines.join("\n"));
1528
1789
  console.log(`\n\x1b[2mPulling ${model}... this may take a few minutes.\x1b[0m`);
1529
- result = await withSpinner(`Pulling ${model}...`, () => runLocalCommand(`ollama pull ${model} 2>&1`, 300_000));
1790
+ result = await withSpinner(`Pulling ${model}...`, () => runOllama(`pull ${model}`));
1530
1791
  return result;
1531
1792
  }
1532
1793
  // Ollama storage — check location & disk usage
@@ -1762,7 +2023,7 @@ expect eof
1762
2023
  const model = fields.model ?? intent.rawText.match(/(?:remove|delete|rm)\s+(?:ollama\s+(?:model\s+)?)?(\S+)/i)?.[1];
1763
2024
  if (!model)
1764
2025
  return `\x1b[33mUsage: ollama remove <model>\x1b[0m\n\x1b[2m Example: "ollama remove llama3.2"\x1b[0m`;
1765
- result = await withSpinner(`Removing ${model}...`, () => runLocalCommand(`ollama rm ${model} 2>&1`, 30_000));
2026
+ result = await withSpinner(`Removing ${model}...`, () => runOllama(`rm ${model}`));
1766
2027
  return result.includes("deleted") ? `\x1b[32m✓\x1b[0m Model ${model} removed.` : result;
1767
2028
  }
1768
2029
  // Codex CLI handlers
@@ -3104,6 +3365,87 @@ expect eof
3104
3365
  }
3105
3366
  return "";
3106
3367
  }
3368
+ // File organization — uses LLM to categorize and move files
3369
+ if (intent.intent === "files.organize") {
3370
+ const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m" };
3371
+ const cwd = process.cwd();
3372
+ const fileList = await runLocalCommand(`ls -1 "${cwd}" 2>/dev/null`).catch(() => "");
3373
+ const fileCount = fileList.trim().split("\n").filter(Boolean).length;
3374
+ if (fileCount === 0)
3375
+ return `${cc.yellow}⚠${cc.reset} No files in current directory.`;
3376
+ if (fileCount < 3)
3377
+ return `${cc.dim}Only ${fileCount} files — not much to organize.${cc.reset}`;
3378
+ console.log(`${cc.cyan}Analyzing ${fileCount} files in ${cwd}...${cc.reset}`);
3379
+ try {
3380
+ const ollamaModel = process.env.NOTOKEN_OLLAMA_MODEL ?? "llama3.2";
3381
+ const prompt = `Files in folder:\n${fileList}\nGive me ONLY shell commands to organize these into directories. No explanation.\nStart with mkdir -p commands, then mv commands. Always include an Uncategorized folder for anything unclear.\nExample:\nmkdir -p photos videos documents Uncategorized\nmv photo.jpg photos/\nmv unknown.dat Uncategorized/\n\nOutput ONLY commands:`;
3382
+ const controller = new AbortController();
3383
+ const timeout = setTimeout(() => controller.abort(), 120_000);
3384
+ const resp = await fetch("http://127.0.0.1:11434/api/generate", {
3385
+ method: "POST",
3386
+ headers: { "Content-Type": "application/json" },
3387
+ signal: controller.signal,
3388
+ body: JSON.stringify({ model: ollamaModel, prompt, stream: false, options: { temperature: 0.1, num_predict: 4096 } }),
3389
+ });
3390
+ clearTimeout(timeout);
3391
+ const data = await resp.json();
3392
+ const commands = (data.response ?? "").split("\n").map((l) => l.trim()).filter((l) => l.startsWith("mkdir") || l.startsWith("mv "));
3393
+ const mkdirs = commands.filter((c) => c.startsWith("mkdir"));
3394
+ const mvs = commands.filter((c) => c.startsWith("mv"));
3395
+ if (commands.length === 0)
3396
+ return `${cc.yellow}⚠${cc.reset} LLM couldn't generate organization commands. Try being more specific.`;
3397
+ const dirs = mkdirs.join(" ").match(/[\w-]+/g)?.filter((d) => d !== "mkdir" && d !== "-p") ?? [];
3398
+ const lines = [`\n${cc.bold}${cc.cyan}── File Organization Plan ──${cc.reset}\n`];
3399
+ lines.push(` ${cc.bold}${fileCount} files${cc.reset} → ${cc.bold}${dirs.length} directories${cc.reset}\n`);
3400
+ lines.push(` ${cc.bold}Directories:${cc.reset} ${dirs.map((d) => `📁 ${d}`).join(" ")}\n`);
3401
+ lines.push(` ${cc.bold}Sample moves:${cc.reset}`);
3402
+ for (const mv of mvs.slice(0, 10)) {
3403
+ const parts = mv.match(/mv\s+(\S+)\s+(\S+)/);
3404
+ if (parts)
3405
+ lines.push(` ${cc.dim}${parts[1]}${cc.reset} → ${cc.cyan}${parts[2]}${cc.reset}`);
3406
+ }
3407
+ if (mvs.length > 10)
3408
+ lines.push(` ${cc.dim}... and ${mvs.length - 10} more moves${cc.reset}`);
3409
+ console.log(lines.join("\n"));
3410
+ suggestAction({ action: `cd "${cwd}" && ${commands.join(" && ")}`, description: `Organize ${fileCount} files into ${dirs.length} folders`, type: "command" });
3411
+ return `\n ${cc.bold}Execute this plan?${cc.reset} Say "yes" to proceed, or "cancel" to abort.`;
3412
+ }
3413
+ catch (err) {
3414
+ return `${cc.red}✗${cc.reset} LLM error: ${err.message?.substring(0, 100)}\n${cc.dim}Make sure Ollama is running.${cc.reset}`;
3415
+ }
3416
+ }
3417
+ // File placement — suggest where a file should go
3418
+ if (intent.intent === "files.place") {
3419
+ const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", cyan: "\x1b[36m" };
3420
+ const cwd = process.cwd();
3421
+ const dirStructure = await runLocalCommand(`find "${cwd}" -maxdepth 2 -type d 2>/dev/null | head -30`).catch(() => "");
3422
+ const fileMatch = intent.rawText.match(/(?:put|place|file)\s+(\S+\.\w+)/i) ?? intent.rawText.match(/(\S+\.\w{2,4})$/);
3423
+ const fileName = fileMatch?.[1] ?? "this file";
3424
+ try {
3425
+ const ollamaModel = process.env.NOTOKEN_OLLAMA_MODEL ?? "llama3.2";
3426
+ const prompt = `My directory structure:\n${dirStructure}\n\nWhere should I put "${fileName}"?\n\nComplete this JSON:\n\`\`\`json\n{"file": "${fileName}", "destination": "FILL_best_directory", "reason": "FILL_why", "command": "mv ${fileName} FILL/"}\n\`\`\`\nOutput only JSON:`;
3427
+ const controller = new AbortController();
3428
+ const timeout = setTimeout(() => controller.abort(), 60_000);
3429
+ const resp = await fetch("http://127.0.0.1:11434/api/generate", {
3430
+ method: "POST",
3431
+ headers: { "Content-Type": "application/json" },
3432
+ signal: controller.signal,
3433
+ body: JSON.stringify({ model: ollamaModel, prompt, stream: false, options: { temperature: 0.1, num_predict: 200 } }),
3434
+ });
3435
+ clearTimeout(timeout);
3436
+ const data = await resp.json();
3437
+ const jsonMatch = (data.response ?? "").match(/\{[\s\S]*\}/);
3438
+ if (jsonMatch) {
3439
+ const parsed = JSON.parse(jsonMatch[0]);
3440
+ suggestAction({ action: parsed.command, description: `Move ${fileName} to ${parsed.destination}`, type: "command" });
3441
+ return `\n ${cc.bold}${parsed.file}${cc.reset} → ${cc.cyan}${parsed.destination}${cc.reset}\n ${cc.dim}${parsed.reason}${cc.reset}\n\n ${cc.dim}Say "yes" to move it.${cc.reset}`;
3442
+ }
3443
+ return `${cc.dim}${(data.response ?? "").substring(0, 200)}${cc.reset}`;
3444
+ }
3445
+ catch (err) {
3446
+ return `${cc.dim}Could not determine placement: ${err.message?.substring(0, 80)}${cc.reset}`;
3447
+ }
3448
+ }
3107
3449
  // Casual chat responses — loaded from config/chat-responses.json
3108
3450
  if (intent.intent.startsWith("chat.")) {
3109
3451
  const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", cyan: "\x1b[36m", yellow: "\x1b[33m" };
@@ -3185,6 +3527,99 @@ expect eof
3185
3527
  // Fallback if no JSON responses loaded
3186
3528
  return `${cc.cyan}I'm NoToken.${cc.reset} Type "help" to see what I can do.`;
3187
3529
  }
3530
+ // ── Tool uninstall ──
3531
+ if (intent.intent === "tool.uninstall") {
3532
+ let toolName = resolveToolName(intent.rawText) || (fields.tool ?? "").toLowerCase();
3533
+ toolName = TOOL_ALIASES[toolName] ?? toolName;
3534
+ const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m" };
3535
+ if (!toolName || !INSTALL_INFO[toolName]) {
3536
+ return `${cc.red}✗ Unknown tool: "${toolName || "?"}"\x1b[0m\n\n ${cc.dim}Available: ${Object.keys(INSTALL_INFO).join(", ")}${cc.reset}`;
3537
+ }
3538
+ const info = INSTALL_INFO[toolName];
3539
+ // Check if installed
3540
+ const existing = await runLocalCommand(info.check + " 2>/dev/null").catch(() => "");
3541
+ if (!existing) {
3542
+ return `${cc.dim}${info.name} is not installed — nothing to uninstall.${cc.reset}`;
3543
+ }
3544
+ console.log(`\n${cc.cyan}Uninstalling ${info.name}...${cc.reset}`);
3545
+ // Determine uninstall command
3546
+ let uninstallCmd = "";
3547
+ if (info.install.startsWith("npm ")) {
3548
+ // npm-based tools — npm uninstall -g
3549
+ const pkg = info.install.replace("npm install -g ", "");
3550
+ uninstallCmd = `npm uninstall -g ${pkg}`;
3551
+ }
3552
+ else if (toolName === "ollama") {
3553
+ if (process.platform === "win32") {
3554
+ uninstallCmd = `powershell -Command "Get-Process ollama -ErrorAction SilentlyContinue | Stop-Process -Force" 2>/dev/null; rm -rf "$(cygpath '$LOCALAPPDATA')/Programs/Ollama" 2>/dev/null; rm -rf "$(cygpath '$LOCALAPPDATA')/Ollama" 2>/dev/null`;
3555
+ }
3556
+ else {
3557
+ uninstallCmd = "sudo systemctl stop ollama 2>/dev/null; sudo rm -f /usr/local/bin/ollama; sudo rm -rf /usr/share/ollama";
3558
+ }
3559
+ }
3560
+ else if (toolName === "docker") {
3561
+ uninstallCmd = process.platform === "win32"
3562
+ ? `powershell -Command "echo 'Uninstall Docker Desktop from Settings > Apps'"`
3563
+ : "sudo apt-get remove -y docker-ce docker-ce-cli containerd.io 2>/dev/null || sudo dnf remove -y docker-ce docker-ce-cli containerd.io 2>/dev/null";
3564
+ }
3565
+ else {
3566
+ uninstallCmd = `echo '${info.name} — uninstall manually'`;
3567
+ }
3568
+ // For openclaw — also stop the gateway first
3569
+ if (toolName === "openclaw") {
3570
+ if (process.platform === "win32") {
3571
+ await runLocalCommand(`powershell -Command "Get-WmiObject Win32_Process -Filter \\"Name='node.exe'\\" | Where-Object { \\$_.CommandLine -match 'openclaw.*gateway' } | ForEach-Object { \\$_.Terminate() }" 2>/dev/null`).catch(() => "");
3572
+ }
3573
+ else {
3574
+ await runLocalCommand("pkill -f openclaw-gateway 2>/dev/null").catch(() => "");
3575
+ }
3576
+ await runLocalCommand("sleep 2").catch(() => { });
3577
+ console.log(`${cc.dim}Gateway stopped${cc.reset}`);
3578
+ }
3579
+ try {
3580
+ result = await withSpinner(`Uninstalling ${info.name}...`, () => runLocalCommand(uninstallCmd + " 2>&1", 120_000));
3581
+ // Verify removal
3582
+ const stillExists = await runLocalCommand(info.check + " 2>/dev/null").catch(() => "");
3583
+ if (!stillExists) {
3584
+ return `${cc.green}✓${cc.reset} ${info.name} uninstalled successfully.`;
3585
+ }
3586
+ return `${cc.yellow}⚠${cc.reset} Uninstall ran but ${info.name} may still be present.\n ${cc.dim}Try manually: ${uninstallCmd}${cc.reset}`;
3587
+ }
3588
+ catch (err) {
3589
+ return `${cc.red}✗ Uninstall failed:${cc.reset} ${err.message.split("\n")[0]}\n ${cc.dim}Try manually: ${uninstallCmd}${cc.reset}`;
3590
+ }
3591
+ }
3592
+ // ── Tool reinstall (uninstall + install) ──
3593
+ if (intent.intent === "tool.reinstall") {
3594
+ let toolName = resolveToolName(intent.rawText) || (fields.tool ?? "").toLowerCase();
3595
+ toolName = TOOL_ALIASES[toolName] ?? toolName;
3596
+ const cc = { reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m" };
3597
+ if (!toolName || !INSTALL_INFO[toolName]) {
3598
+ return `${cc.red}✗ Unknown tool: "${toolName || "?"}"\x1b[0m\n\n ${cc.dim}Available: ${Object.keys(INSTALL_INFO).join(", ")}${cc.reset}`;
3599
+ }
3600
+ const info = INSTALL_INFO[toolName];
3601
+ console.log(`\n${cc.cyan}Reinstalling ${info.name}...${cc.reset}\n`);
3602
+ // Uninstall first
3603
+ if (info.install.startsWith("npm ")) {
3604
+ const pkg = info.install.replace("npm install -g ", "");
3605
+ console.log(`${cc.dim}Uninstalling...${cc.reset}`);
3606
+ // Stop openclaw gateway if reinstalling openclaw
3607
+ if (toolName === "openclaw") {
3608
+ if (process.platform === "win32") {
3609
+ await runLocalCommand(`powershell -Command "Get-WmiObject Win32_Process -Filter \\"Name='node.exe'\\" | Where-Object { \\$_.CommandLine -match 'openclaw.*gateway' } | ForEach-Object { \\$_.Terminate() }" 2>/dev/null`).catch(() => "");
3610
+ }
3611
+ else {
3612
+ await runLocalCommand("pkill -f openclaw-gateway 2>/dev/null").catch(() => "");
3613
+ }
3614
+ await runLocalCommand("sleep 2").catch(() => { });
3615
+ }
3616
+ await withSpinner(`Uninstalling ${info.name}...`, () => runLocalCommand(`npm uninstall -g ${pkg} 2>&1`, 60_000)).catch(() => "");
3617
+ }
3618
+ // Install fresh — reuse the tool.install intent
3619
+ console.log(`${cc.dim}Installing fresh...${cc.reset}`);
3620
+ const fakeIntent = { ...intent, intent: "tool.install" };
3621
+ return executeIntent(fakeIntent);
3622
+ }
3188
3623
  // Entity define/list
3189
3624
  if (intent.intent === "entity.define")
3190
3625
  return learnEntity(intent.rawText) ?? "Could not understand. Try: 'metroplex is 66.94.115.165'";
package/dist/index.d.ts CHANGED
@@ -16,13 +16,16 @@ export { findSimilarIntents, phraseSimilarity, expandWithCooccurrences } from ".
16
16
  export { loadKnowledgeGraph, saveKnowledgeGraph, addEntity, addRelation, getEntity, getRelated, resolveReference, resolveCandidates, inferIntent, queryGraph, rebuildGraph, learnFromExecution, flushGraph } from "./nlp/knowledgeGraph.js";
17
17
  export { expandQuery, findCluster, suggestIntents, clusterWords } from "./nlp/conceptExpansion.js";
18
18
  export { analyzeUncertainty, getUncoveredSpans } from "./nlp/uncertainty.js";
19
- export { llmFallback, isLLMConfigured, getLLMBackend, formatLLMFallback } from "./nlp/llmFallback.js";
19
+ export { llmFallback, llmMultiTurn, isLLMConfigured, getLLMBackend, formatLLMFallback, addLLMContext, clearLLMContext } from "./nlp/llmFallback.js";
20
20
  export { suggestEntityCorrection, correctEntities, resolveDescription, resetEntityVocab } from "./nlp/entitySpellCorrect.js";
21
21
  export { recordOutcome, getMultiplier, calibrateVotes, recordCorrection, getCalibrationStats, flushCalibration } from "./nlp/confidenceCalibrator.js";
22
22
  export { detectBatch, expandBatch, expandEnvironmentBatch } from "./nlp/batchParser.js";
23
23
  export { getCurrentTopic, suggestFollowups, getTopicDefault } from "./conversation/topicTracker.js";
24
24
  export { progressReporter, reportProgress, reportStep } from "./utils/progressReporter.js";
25
25
  export { loadHistory as loadCommandHistory, addToHistory, searchHistory as searchCommandHistory, getRecentCommands, getReadlineHistory } from "./utils/commandHistory.js";
26
+ export { getUserContext, findFreshestClaudeToken, detectUserMismatch, getAuthProfilesPath, resetUserContext } from "./utils/userContext.js";
27
+ export { parseOpenclawModels, parseOpenclawStatus, parseOpenclawDeepStatus } from "./utils/openclawDiag.js";
28
+ export { parseLogLine, analyzeLogs, formatLogAnalysis } from "./utils/openclawLogParser.js";
26
29
  export { recordCommand, getAchievements, getUsageStats, flushStats } from "./utils/achievements.js";
27
30
  export { teachCommand, getLearnedCommand, listLearnedCommands, forgetCommand, parseTeachStatement } from "./utils/teachMode.js";
28
31
  export { executeIntent, getRandomTip } from "./handlers/executor.js";
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ export { findSimilarIntents, phraseSimilarity, expandWithCooccurrences } from ".
17
17
  export { loadKnowledgeGraph, saveKnowledgeGraph, addEntity, addRelation, getEntity, getRelated, resolveReference, resolveCandidates, inferIntent, queryGraph, rebuildGraph, learnFromExecution, flushGraph } from "./nlp/knowledgeGraph.js";
18
18
  export { expandQuery, findCluster, suggestIntents, clusterWords } from "./nlp/conceptExpansion.js";
19
19
  export { analyzeUncertainty, getUncoveredSpans } from "./nlp/uncertainty.js";
20
- export { llmFallback, isLLMConfigured, getLLMBackend, formatLLMFallback } from "./nlp/llmFallback.js";
20
+ export { llmFallback, llmMultiTurn, isLLMConfigured, getLLMBackend, formatLLMFallback, addLLMContext, clearLLMContext } from "./nlp/llmFallback.js";
21
21
  export { suggestEntityCorrection, correctEntities, resolveDescription, resetEntityVocab } from "./nlp/entitySpellCorrect.js";
22
22
  export { recordOutcome, getMultiplier, calibrateVotes, recordCorrection, getCalibrationStats, flushCalibration } from "./nlp/confidenceCalibrator.js";
23
23
  export { detectBatch, expandBatch, expandEnvironmentBatch } from "./nlp/batchParser.js";
@@ -25,6 +25,10 @@ export { getCurrentTopic, suggestFollowups, getTopicDefault } from "./conversati
25
25
  // ── Progress & History ──
26
26
  export { progressReporter, reportProgress, reportStep } from "./utils/progressReporter.js";
27
27
  export { loadHistory as loadCommandHistory, addToHistory, searchHistory as searchCommandHistory, getRecentCommands, getReadlineHistory } from "./utils/commandHistory.js";
28
+ // ── User Context & OpenClaw Parsing ──
29
+ export { getUserContext, findFreshestClaudeToken, detectUserMismatch, getAuthProfilesPath, resetUserContext } from "./utils/userContext.js";
30
+ export { parseOpenclawModels, parseOpenclawStatus, parseOpenclawDeepStatus } from "./utils/openclawDiag.js";
31
+ export { parseLogLine, analyzeLogs, formatLogAnalysis } from "./utils/openclawLogParser.js";
28
32
  // ── Achievements & Teach ──
29
33
  export { recordCommand, getAchievements, getUsageStats, flushStats } from "./utils/achievements.js";
30
34
  export { teachCommand, getLearnedCommand, listLearnedCommands, forgetCommand, parseTeachStatement } from "./utils/teachMode.js";
@@ -22,8 +22,27 @@ export interface LLMFallbackResult {
22
22
  intent?: string;
23
23
  command?: string;
24
24
  }>;
25
+ /** Raw shell commands to run if no intent matches */
26
+ shellCommands?: string[];
27
+ /** Questions to ask the user for clarification */
25
28
  missingInfo?: string[];
29
+ /** Commands to run first to gather info before answering (multi-turn) */
30
+ gatherCommands?: Array<{
31
+ command: string;
32
+ purpose: string;
33
+ }>;
34
+ /** Whether the LLM needs more info from command outputs before it can answer */
35
+ needsMoreInfo?: boolean;
26
36
  }
37
+ /** Add a turn to the LLM conversation for multi-turn context */
38
+ export declare function addLLMContext(role: "user" | "assistant", content: string): void;
39
+ /** Clear LLM conversation when topic changes */
40
+ export declare function clearLLMContext(): void;
41
+ /** Get conversation for context in multi-turn */
42
+ export declare function getLLMContext(): Array<{
43
+ role: "user" | "assistant";
44
+ content: string;
45
+ }>;
27
46
  /**
28
47
  * Check if any LLM is configured.
29
48
  */
@@ -47,6 +66,34 @@ export declare function llmFallback(rawText: string, context: {
47
66
  type: string;
48
67
  }>;
49
68
  uncertainTokens?: string[];
69
+ nearMisses?: Array<{
70
+ intent: string;
71
+ score: number;
72
+ source: string;
73
+ }>;
74
+ }): Promise<LLMFallbackResult | null>;
75
+ /**
76
+ * Multi-turn LLM disambiguation.
77
+ *
78
+ * 1. Ask the LLM what to do
79
+ * 2. If it needs more info → run gatherCommands → feed results back
80
+ * 3. Repeat up to maxTurns times
81
+ * 4. Return the final result
82
+ */
83
+ export declare function llmMultiTurn(rawText: string, context: {
84
+ recentIntents?: string[];
85
+ knownEntities?: Array<{
86
+ entity: string;
87
+ type: string;
88
+ }>;
89
+ nearMisses?: Array<{
90
+ intent: string;
91
+ score: number;
92
+ source: string;
93
+ }>;
94
+ }, options?: {
95
+ maxTurns?: number;
96
+ onProgress?: (msg: string) => void;
50
97
  }): Promise<LLMFallbackResult | null>;
51
98
  /**
52
99
  * Check if Ollama is installed (not just running).