notoken-core 1.5.1 → 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 +5023 -65
  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
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Automated Discord bot setup via Playwright.
3
+ *
4
+ * Opens the Discord Developer Portal, walks the user through login,
5
+ * then automates: create application, create bot, copy token, enable
6
+ * Message Content Intent, generate OAuth2 invite URL, open it.
7
+ *
8
+ * The user only needs to:
9
+ * 1. Log in to Discord (notoken never touches credentials)
10
+ * 2. Pick which server to add the bot to
11
+ *
12
+ * Returns the bot token for OpenClaw channel registration.
13
+ */
14
+ const c = {
15
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
16
+ green: "\x1b[32m", yellow: "\x1b[33m", red: "\x1b[31m", cyan: "\x1b[36m",
17
+ };
18
+ /**
19
+ * Detect available browser executable on the system.
20
+ * Prefers Edge (available on Windows), falls back to Chrome, then Chromium.
21
+ */
22
+ async function findBrowserPath() {
23
+ const { execSync } = await import("node:child_process");
24
+ const candidates = [
25
+ // Windows browsers via WSL
26
+ "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe",
27
+ "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe",
28
+ "/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe",
29
+ // Linux browsers
30
+ "/usr/bin/google-chrome",
31
+ "/usr/bin/chromium-browser",
32
+ "/usr/bin/chromium",
33
+ "/usr/bin/microsoft-edge",
34
+ ];
35
+ for (const path of candidates) {
36
+ try {
37
+ execSync(`ls "${path}" 2>/dev/null`, { stdio: "pipe" });
38
+ return path;
39
+ }
40
+ catch { /* not found */ }
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Run the automated Discord bot setup.
46
+ *
47
+ * @param appName - Name for the Discord application (default: "OpenClaw")
48
+ * @param headless - Run headless (default: false — user needs to see login)
49
+ */
50
+ export async function automateDiscordBotSetup(appName = "OpenClaw", headless = false) {
51
+ // Dynamic import — Playwright is optional, not a core dependency
52
+ let playwright;
53
+ try {
54
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
55
+ playwright = await Function('return import("playwright")')();
56
+ }
57
+ catch {
58
+ return {
59
+ success: false,
60
+ error: "Playwright not installed. Run: npm install -g playwright && npx playwright install chromium",
61
+ };
62
+ }
63
+ const browserPath = await findBrowserPath();
64
+ console.log(`\n${c.bold}${c.cyan}── Discord Bot Setup ──${c.reset}\n`);
65
+ let browser;
66
+ let context;
67
+ let page;
68
+ try {
69
+ // Launch browser — use Windows Edge if available for visible UI
70
+ console.log(` ${c.dim}Launching browser...${c.reset}`);
71
+ if (browserPath?.includes("/mnt/c/")) {
72
+ // Windows browser via WSL — use channel launch
73
+ const winPath = browserPath
74
+ .replace("/mnt/c/", "C:\\")
75
+ .replace(/\//g, "\\");
76
+ browser = await playwright.chromium.launch({
77
+ headless: false, // Must be visible for user login
78
+ executablePath: browserPath,
79
+ args: ["--no-sandbox"],
80
+ });
81
+ }
82
+ else {
83
+ browser = await playwright.chromium.launch({
84
+ headless,
85
+ args: ["--no-sandbox"],
86
+ });
87
+ }
88
+ context = await browser.newContext({
89
+ viewport: { width: 1280, height: 900 },
90
+ });
91
+ page = await context.newPage();
92
+ // ── Step 1: Navigate to Discord Developer Portal ──
93
+ console.log(` ${c.bold}1.${c.reset} Opening Discord Developer Portal...`);
94
+ await page.goto("https://discord.com/developers/applications", {
95
+ waitUntil: "networkidle",
96
+ timeout: 30_000,
97
+ });
98
+ // Check if login is needed
99
+ const url = page.url();
100
+ if (url.includes("/login")) {
101
+ console.log(`\n ${c.yellow}${c.bold}Please log in to Discord in the browser window.${c.reset}`);
102
+ console.log(` ${c.dim}Waiting for you to complete login...${c.reset}\n`);
103
+ // Wait for redirect to developer portal after login (up to 5 minutes)
104
+ await page.waitForURL("**/developers/applications**", { timeout: 300_000 });
105
+ console.log(` ${c.green}✓${c.reset} Logged in successfully!\n`);
106
+ }
107
+ else {
108
+ console.log(` ${c.green}✓${c.reset} Already logged in.\n`);
109
+ }
110
+ // ── Step 2: Create New Application ──
111
+ console.log(` ${c.bold}2.${c.reset} Creating application "${appName}"...`);
112
+ await page.waitForTimeout(2000);
113
+ // Click "New Application" button
114
+ const newAppBtn = page.locator('button:has-text("New Application"), div[class*="actionButton"]:has-text("New Application")');
115
+ await newAppBtn.waitFor({ timeout: 10_000 });
116
+ await newAppBtn.click();
117
+ // Fill in the application name
118
+ await page.waitForTimeout(1000);
119
+ const nameInput = page.locator('input[placeholder*="name"], input[name="name"]').first();
120
+ await nameInput.waitFor({ timeout: 5_000 });
121
+ await nameInput.fill(appName);
122
+ // Check the ToS checkbox if present
123
+ const tosCheckbox = page.locator('input[type="checkbox"], label:has-text("policy")').first();
124
+ if (await tosCheckbox.isVisible({ timeout: 2000 }).catch(() => false)) {
125
+ await tosCheckbox.click();
126
+ }
127
+ // Click Create
128
+ const createBtn = page.locator('button:has-text("Create")').first();
129
+ await createBtn.click();
130
+ await page.waitForTimeout(3000);
131
+ // Get the application ID from the URL
132
+ const appUrl = page.url();
133
+ const appIdMatch = appUrl.match(/applications\/(\d+)/);
134
+ const applicationId = appIdMatch?.[1] ?? "";
135
+ console.log(` ${c.green}✓${c.reset} Application created${applicationId ? ` (ID: ${applicationId})` : ""}\n`);
136
+ // ── Step 3: Navigate to Bot tab and create bot ──
137
+ console.log(` ${c.bold}3.${c.reset} Setting up bot...`);
138
+ // Click "Bot" in sidebar
139
+ const botTab = page.locator('a:has-text("Bot"), div[class*="item"]:has-text("Bot")').first();
140
+ await botTab.click();
141
+ await page.waitForTimeout(2000);
142
+ // Click "Reset Token" or "Add Bot" if needed
143
+ const resetTokenBtn = page.locator('button:has-text("Reset Token")').first();
144
+ const addBotBtn = page.locator('button:has-text("Add Bot")').first();
145
+ if (await addBotBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
146
+ await addBotBtn.click();
147
+ await page.waitForTimeout(1000);
148
+ // Confirm
149
+ const confirmBtn = page.locator('button:has-text("Yes, do it!")').first();
150
+ if (await confirmBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
151
+ await confirmBtn.click();
152
+ }
153
+ await page.waitForTimeout(2000);
154
+ }
155
+ // Reset/reveal token
156
+ if (await resetTokenBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
157
+ await resetTokenBtn.click();
158
+ await page.waitForTimeout(1000);
159
+ // Confirm reset
160
+ const confirmReset = page.locator('button:has-text("Yes, do it!")').first();
161
+ if (await confirmReset.isVisible({ timeout: 2000 }).catch(() => false)) {
162
+ await confirmReset.click();
163
+ }
164
+ }
165
+ // Wait for token to appear and copy it
166
+ await page.waitForTimeout(2000);
167
+ let botToken = "";
168
+ // Try to find the token in an input or code element
169
+ const tokenInput = page.locator('input[value*="."], span[class*="token"], div[class*="token"] input').first();
170
+ if (await tokenInput.isVisible({ timeout: 3000 }).catch(() => false)) {
171
+ botToken = await tokenInput.inputValue().catch(() => "");
172
+ if (!botToken) {
173
+ botToken = await tokenInput.textContent().catch(() => "") ?? "";
174
+ }
175
+ }
176
+ // Try clicking "Copy" button if token not grabbed directly
177
+ if (!botToken) {
178
+ const copyBtn = page.locator('button:has-text("Copy")').first();
179
+ if (await copyBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
180
+ await copyBtn.click();
181
+ // Token is now in clipboard — try to read it
182
+ try {
183
+ botToken = await page.evaluate(() => navigator.clipboard.readText());
184
+ }
185
+ catch {
186
+ // Clipboard access denied — ask user
187
+ }
188
+ }
189
+ }
190
+ if (botToken) {
191
+ console.log(` ${c.green}✓${c.reset} Bot token captured\n`);
192
+ }
193
+ else {
194
+ console.log(` ${c.yellow}⚠${c.reset} Could not auto-capture token.`);
195
+ console.log(` ${c.bold}Please copy the bot token from the browser and paste it here.${c.reset}\n`);
196
+ }
197
+ // ── Step 4: Enable Message Content Intent ──
198
+ console.log(` ${c.bold}4.${c.reset} Enabling Message Content Intent...`);
199
+ // Scroll down to Privileged Gateway Intents
200
+ await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
201
+ await page.waitForTimeout(1000);
202
+ // Find and enable Message Content Intent toggle
203
+ const messageContentLabel = page.locator('text=Message Content Intent, label:has-text("MESSAGE CONTENT INTENT")').first();
204
+ if (await messageContentLabel.isVisible({ timeout: 3000 }).catch(() => false)) {
205
+ // Find the toggle near this label
206
+ const toggle = page.locator('div:has-text("MESSAGE CONTENT INTENT") input[type="checkbox"], div:has-text("Message Content Intent") [role="switch"]').first();
207
+ if (await toggle.isVisible({ timeout: 2000 }).catch(() => false)) {
208
+ const isChecked = await toggle.isChecked().catch(() => false);
209
+ if (!isChecked) {
210
+ await toggle.click();
211
+ await page.waitForTimeout(500);
212
+ }
213
+ }
214
+ }
215
+ // Save changes
216
+ const saveBtn = page.locator('button:has-text("Save Changes")').first();
217
+ if (await saveBtn.isVisible({ timeout: 2000 }).catch(() => false)) {
218
+ await saveBtn.click();
219
+ await page.waitForTimeout(1000);
220
+ }
221
+ console.log(` ${c.green}✓${c.reset} Message Content Intent enabled\n`);
222
+ // ── Step 5: Generate OAuth2 invite URL ──
223
+ console.log(` ${c.bold}5.${c.reset} Generating invite URL...`);
224
+ // Navigate to OAuth2 → URL Generator
225
+ const oauth2Tab = page.locator('a:has-text("OAuth2"), div[class*="item"]:has-text("OAuth2")').first();
226
+ await oauth2Tab.click();
227
+ await page.waitForTimeout(1000);
228
+ // Look for URL Generator sub-tab
229
+ const urlGenTab = page.locator('a:has-text("URL Generator"), div:has-text("URL Generator")').first();
230
+ if (await urlGenTab.isVisible({ timeout: 3000 }).catch(() => false)) {
231
+ await urlGenTab.click();
232
+ await page.waitForTimeout(1000);
233
+ }
234
+ // Select "bot" scope
235
+ const botScope = page.locator('label:has-text("bot"), input[value="bot"]').first();
236
+ if (await botScope.isVisible({ timeout: 3000 }).catch(() => false)) {
237
+ await botScope.click();
238
+ await page.waitForTimeout(1000);
239
+ }
240
+ // Select permissions: Send Messages, Read Messages
241
+ for (const perm of ["Send Messages", "Read Message History", "View Channels"]) {
242
+ const permLabel = page.locator(`label:has-text("${perm}")`).first();
243
+ if (await permLabel.isVisible({ timeout: 1000 }).catch(() => false)) {
244
+ await permLabel.click();
245
+ await page.waitForTimeout(300);
246
+ }
247
+ }
248
+ // Copy the generated URL
249
+ await page.waitForTimeout(1000);
250
+ let inviteUrl = "";
251
+ const urlInput = page.locator('input[value*="discord.com/oauth2"], input[value*="discord.com/api/oauth2"]').first();
252
+ if (await urlInput.isVisible({ timeout: 3000 }).catch(() => false)) {
253
+ inviteUrl = await urlInput.inputValue();
254
+ }
255
+ if (inviteUrl) {
256
+ console.log(` ${c.green}✓${c.reset} Invite URL generated\n`);
257
+ // ── Step 6: Open invite URL to add bot to server ──
258
+ console.log(` ${c.bold}6.${c.reset} Opening bot invite page...`);
259
+ console.log(` ${c.yellow}${c.bold}Pick your Discord server in the browser to add the bot.${c.reset}\n`);
260
+ await page.goto(inviteUrl);
261
+ // Wait for user to authorize (page changes after clicking Authorize)
262
+ await page.waitForURL("**/oauth2/authorized**", { timeout: 120_000 }).catch(() => { });
263
+ console.log(` ${c.green}✓${c.reset} Bot added to server!\n`);
264
+ }
265
+ else {
266
+ console.log(` ${c.yellow}⚠${c.reset} Could not auto-generate invite URL.`);
267
+ if (applicationId) {
268
+ inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${applicationId}&permissions=68608&scope=bot`;
269
+ console.log(` ${c.dim}Manual invite URL: ${inviteUrl}${c.reset}\n`);
270
+ }
271
+ }
272
+ // Close browser
273
+ await browser.close();
274
+ return {
275
+ success: !!botToken,
276
+ botToken: botToken || undefined,
277
+ applicationId: applicationId || undefined,
278
+ inviteUrl: inviteUrl || undefined,
279
+ error: botToken ? undefined : "Could not auto-capture bot token. Please copy it manually from the Discord Developer Portal.",
280
+ };
281
+ }
282
+ catch (err) {
283
+ const msg = err.message ?? String(err);
284
+ console.log(`\n ${c.red}✗ Automation error: ${msg.split("\n")[0]}${c.reset}`);
285
+ // Try to close browser gracefully
286
+ try {
287
+ await browser?.close();
288
+ }
289
+ catch { /* */ }
290
+ return { success: false, error: msg };
291
+ }
292
+ }
293
+ /**
294
+ * Full Discord setup flow — automate browser + register with OpenClaw.
295
+ */
296
+ export async function setupDiscordChannel(appName = "OpenClaw") {
297
+ const result = await automateDiscordBotSetup(appName);
298
+ if (result.success && result.botToken) {
299
+ // Register with OpenClaw
300
+ console.log(`${c.bold}${c.cyan}── Registering with OpenClaw ──${c.reset}\n`);
301
+ try {
302
+ const { execSync } = await import("node:child_process");
303
+ const nvmPrefix = `for d in "$HOME/.nvm" "/home/"*"/.nvm" "/root/.nvm"; do [ -s "$d/nvm.sh" ] && export NVM_DIR="$d" && . "$d/nvm.sh" && break; done 2>/dev/null; nvm use 22 > /dev/null 2>&1;`;
304
+ // Try direct Node 22 path first
305
+ const node22Paths = ["/home/ino/.nvm/versions/node/v22.22.2/bin/node"];
306
+ let node22 = "node";
307
+ for (const p of node22Paths) {
308
+ try {
309
+ execSync(`ls "${p}"`, { stdio: "pipe" });
310
+ node22 = p;
311
+ break;
312
+ }
313
+ catch { /* */ }
314
+ }
315
+ const ocBin = execSync("readlink -f $(which openclaw) 2>/dev/null || which openclaw", { encoding: "utf-8" }).trim();
316
+ execSync(`${node22} ${ocBin} channels add --channel discord --token "${result.botToken}"`, { stdio: "inherit", timeout: 15_000 });
317
+ console.log(`\n ${c.green}✓${c.reset} Discord channel registered with OpenClaw!`);
318
+ // Restart gateway to pick up new channel
319
+ console.log(` ${c.dim}Restarting gateway...${c.reset}`);
320
+ execSync("pkill -f openclaw-gateway 2>/dev/null", { stdio: "pipe" }).toString();
321
+ return [
322
+ `\n${c.green}${c.bold}✓ Discord bot setup complete!${c.reset}\n`,
323
+ ` ${c.bold}Bot:${c.reset} ${appName}`,
324
+ result.applicationId ? ` ${c.bold}App ID:${c.reset} ${result.applicationId}` : "",
325
+ ` ${c.bold}Channel:${c.reset} Discord — registered with OpenClaw`,
326
+ `\n ${c.dim}Restart OpenClaw: "restart openclaw"${c.reset}`,
327
+ ` ${c.dim}Then chat with OpenClaw in your Discord server!${c.reset}`,
328
+ ].filter(Boolean).join("\n");
329
+ }
330
+ catch (err) {
331
+ return `${c.yellow}⚠${c.reset} Bot created but OpenClaw registration failed.\n Token: ${result.botToken}\n ${c.dim}Register manually: openclaw channels add --channel discord --token ${result.botToken}${c.reset}`;
332
+ }
333
+ }
334
+ if (result.error) {
335
+ return `${c.yellow}⚠${c.reset} ${result.error}\n\n ${c.dim}If you have the token, say: "setup discord with token YOUR_TOKEN"${c.reset}`;
336
+ }
337
+ return `${c.red}✗ Discord setup failed.${c.reset}`;
338
+ }
@@ -13,7 +13,7 @@ import { getLastEntity, getRecentTurns } from "./store.js";
13
13
  */
14
14
  // Patterns that signal a reference to a previous turn
15
15
  const REPEAT_PATTERNS = [
16
- /^(do it|do that|run it|run that|same thing|again|repeat|redo|re-?run)\b/i,
16
+ /^(do it|do that|run it|run that|same thing|again|repeat|redo|re-?run|try again|retry|one more time|run again|do it again|try that again|go again|try it now|try now|try it|let.?s try|give it another|another try)\b/i,
17
17
  /^same\b/i,
18
18
  ];
19
19
  const PRONOUN_PATTERNS = [
@@ -24,6 +24,18 @@ const PRONOUN_PATTERNS = [
24
24
  { pattern: /\bthat (file|path|directory)\b/i, refType: "path" },
25
25
  { pattern: /\bthere\b/i, refType: "environment" },
26
26
  { pattern: /\bthat (env|environment|box|machine)\b/i, refType: "environment" },
27
+ // "the other" — refers to the second-most-recent entity (not the one we just acted on)
28
+ { pattern: /\bthe other (one|service|server|thing)\b/i, refType: "service", offset: 1 },
29
+ { pattern: /\bthe other (env|environment|box|machine|server)\b/i, refType: "environment", offset: 1 },
30
+ { pattern: /\bthe other\b/i, refType: "any", offset: 1 },
31
+ { pattern: /\bnot that one\b/i, refType: "any", offset: 1 },
32
+ { pattern: /\bnot this one\b/i, refType: "any", offset: 1 },
33
+ { pattern: /\bno not this one\b/i, refType: "any", offset: 1 },
34
+ { pattern: /\bno not that one\b/i, refType: "any", offset: 1 },
35
+ { pattern: /\bnot that\b/i, refType: "any", offset: 1 },
36
+ { pattern: /\bthe previous one\b/i, refType: "any", offset: 1 },
37
+ { pattern: /\bthe one before\b/i, refType: "any", offset: 1 },
38
+ { pattern: /\bthe first one\b/i, refType: "any", offset: 1 },
27
39
  ];
28
40
  const OVERRIDE_PATTERNS = [
29
41
  { pattern: /\bbut (?:on|in) (\w+)\b/i, field: "environment" },
@@ -42,6 +54,27 @@ export function resolveCoreferences(rawText, conv) {
42
54
  let resolvedIntent;
43
55
  const recentTurns = getRecentTurns(conv, 5);
44
56
  const lastUserTurn = recentTurns[recentTurns.length - 1];
57
+ // 0. Check for "the other thing" / "try the other thing" — second-to-last command
58
+ const otherThingPattern = /^(?:try |do |run )?the other (?:thing|one|command)\b/i;
59
+ if (otherThingPattern.test(rawText.trim())) {
60
+ isReference = true;
61
+ const prevTurn = recentTurns[recentTurns.length - 2];
62
+ if (prevTurn?.intent && prevTurn.fields) {
63
+ resolvedIntent = {
64
+ intent: prevTurn.intent,
65
+ confidence: 0.8,
66
+ rawText,
67
+ fields: { ...prevTurn.fields },
68
+ };
69
+ resolvedText = prevTurn.rawText;
70
+ resolutions.push({
71
+ original: rawText,
72
+ resolved: prevTurn.rawText,
73
+ source: "last_turn",
74
+ });
75
+ }
76
+ return { resolvedText, isReference, resolvedIntent, resolutions };
77
+ }
45
78
  // 1. Check for full repeat patterns ("do it again", "same thing")
46
79
  for (const pattern of REPEAT_PATTERNS) {
47
80
  if (pattern.test(rawText.trim())) {
@@ -73,13 +106,20 @@ export function resolveCoreferences(rawText, conv) {
73
106
  return { resolvedText, isReference, resolvedIntent, resolutions };
74
107
  }
75
108
  }
76
- // 2. Resolve pronouns ("restart it", "check that service")
77
- for (const { pattern, refType } of PRONOUN_PATTERNS) {
109
+ // 2. Resolve pronouns ("restart it", "check that service", "the other one")
110
+ for (const { pattern, refType, offset } of PRONOUN_PATTERNS) {
78
111
  const match = rawText.match(pattern);
79
112
  if (!match)
80
113
  continue;
81
114
  let resolved;
82
- if (refType === "any") {
115
+ if (offset && offset > 0) {
116
+ // "the other" — get the Nth entity (skip the most recent)
117
+ const candidates = refType === "any"
118
+ ? [...conv.knowledgeTree].sort((a, b) => b.lastMentioned - a.lastMentioned)
119
+ : conv.knowledgeTree.filter(n => n.type === refType).sort((a, b) => b.lastMentioned - a.lastMentioned);
120
+ resolved = candidates[offset]; // offset=1 means second-most-recent
121
+ }
122
+ else if (refType === "any") {
83
123
  // "it" → most recent service, then most recent entity
84
124
  resolved = getLastEntity(conv, "service")
85
125
  ?? getLastEntity(conv, "path")
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Pending Actions.
3
+ *
4
+ * Tracks suggestions NoToken makes so when the user says
5
+ * "ok", "try that", "do it", "run it", "yes" — it knows what to execute.
6
+ *
7
+ * Examples:
8
+ * NoToken: "✓ Installed. Start: notoken start stable-diffusion"
9
+ * User: "ok try it"
10
+ * → executes "notoken start stable-diffusion"
11
+ *
12
+ * NoToken: "Update available: 1.5.0 → 1.7.0"
13
+ * User: "yes do it"
14
+ * → executes update
15
+ */
16
+ export interface PendingAction {
17
+ /** What to execute — either an intent name or a raw command */
18
+ action: string;
19
+ /** Human description */
20
+ description: string;
21
+ /** Type: intent to parse, or command to run directly */
22
+ type: "intent" | "command";
23
+ /** When it was suggested (auto-set by suggestAction) */
24
+ timestamp?: number;
25
+ /** The raw text/fields for the intent */
26
+ fields?: Record<string, unknown>;
27
+ }
28
+ /**
29
+ * Store an action that NoToken is suggesting to the user.
30
+ * Most recent is at the end.
31
+ */
32
+ export declare function suggestAction(action: PendingAction): void;
33
+ /**
34
+ * Get the most recent pending action (if any, and if not too old).
35
+ * Actions expire after 5 minutes.
36
+ */
37
+ export declare function getLastPendingAction(): PendingAction | null;
38
+ /**
39
+ * Pop (consume) the last pending action.
40
+ */
41
+ export declare function consumePendingAction(): PendingAction | null;
42
+ /**
43
+ * Check if user is giving a directive about the pending action.
44
+ * E.g. "put it on F drive", "install it on D:", "no use /mnt/f"
45
+ * Returns the resolved new text if yes, null otherwise.
46
+ */
47
+ export declare function isRedirectingPendingAction(text: string): string | null;
48
+ /**
49
+ * Check if user input is an affirmation to execute a pending action.
50
+ */
51
+ export declare function isAffirmation(text: string): boolean;
52
+ /**
53
+ * Clear all pending actions.
54
+ */
55
+ export declare function clearPendingActions(): void;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Pending Actions.
3
+ *
4
+ * Tracks suggestions NoToken makes so when the user says
5
+ * "ok", "try that", "do it", "run it", "yes" — it knows what to execute.
6
+ *
7
+ * Examples:
8
+ * NoToken: "✓ Installed. Start: notoken start stable-diffusion"
9
+ * User: "ok try it"
10
+ * → executes "notoken start stable-diffusion"
11
+ *
12
+ * NoToken: "Update available: 1.5.0 → 1.7.0"
13
+ * User: "yes do it"
14
+ * → executes update
15
+ */
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
17
+ import { resolve } from "node:path";
18
+ import { homedir } from "node:os";
19
+ const PENDING_FILE = resolve(process.env.NOTOKEN_HOME ?? resolve(homedir(), ".notoken"), "pending-actions.json");
20
+ // Persisted to disk so "try it" works across CLI invocations
21
+ let pendingActions = loadFromDisk();
22
+ function loadFromDisk() {
23
+ try {
24
+ if (existsSync(PENDING_FILE)) {
25
+ return JSON.parse(readFileSync(PENDING_FILE, "utf-8"));
26
+ }
27
+ }
28
+ catch { }
29
+ return [];
30
+ }
31
+ function saveToDisk() {
32
+ try {
33
+ const dir = resolve(PENDING_FILE, "..");
34
+ mkdirSync(dir, { recursive: true });
35
+ writeFileSync(PENDING_FILE, JSON.stringify(pendingActions));
36
+ }
37
+ catch { }
38
+ }
39
+ /**
40
+ * Store an action that NoToken is suggesting to the user.
41
+ * Most recent is at the end.
42
+ */
43
+ export function suggestAction(action) {
44
+ pendingActions.push({ ...action, timestamp: Date.now() });
45
+ // Keep last 5
46
+ if (pendingActions.length > 5)
47
+ pendingActions = pendingActions.slice(-5);
48
+ saveToDisk();
49
+ }
50
+ /**
51
+ * Get the most recent pending action (if any, and if not too old).
52
+ * Actions expire after 5 minutes.
53
+ */
54
+ export function getLastPendingAction() {
55
+ if (pendingActions.length === 0)
56
+ return null;
57
+ const last = pendingActions[pendingActions.length - 1];
58
+ // Expire after 5 minutes
59
+ if (Date.now() - (last.timestamp ?? 0) > 5 * 60 * 1000)
60
+ return null;
61
+ return last;
62
+ }
63
+ /**
64
+ * Pop (consume) the last pending action.
65
+ */
66
+ export function consumePendingAction() {
67
+ const action = getLastPendingAction();
68
+ if (action) {
69
+ pendingActions.pop();
70
+ saveToDisk();
71
+ }
72
+ return action;
73
+ }
74
+ /**
75
+ * Check if user is giving a directive about the pending action.
76
+ * E.g. "put it on F drive", "install it on D:", "no use /mnt/f"
77
+ * Returns the resolved new text if yes, null otherwise.
78
+ */
79
+ export function isRedirectingPendingAction(text) {
80
+ const pending = getLastPendingAction();
81
+ if (!pending)
82
+ return null;
83
+ const normalized = text.toLowerCase().trim();
84
+ // "put it on X", "install it on X", "no put it on X", "use X instead"
85
+ const redirectPatterns = [
86
+ /(?:put|install|place|move|set|store)\s+(?:it|that|this)\s+(?:on|in|at|to)\s+(.+)/i,
87
+ /(?:no|nah|nope)\s*,?\s*(?:put|install|place|use|try)\s+(?:it\s+)?(?:on|in|at)?\s*(.+)/i,
88
+ /(?:use|try)\s+(.+?)\s+instead/i,
89
+ /(?:on|in|at)\s+(.+?)\s+(?:drive|folder|directory|instead)/i,
90
+ ];
91
+ for (const pattern of redirectPatterns) {
92
+ const match = normalized.match(pattern);
93
+ if (match) {
94
+ const location = match[1].trim();
95
+ // Re-form the pending action with the new location
96
+ if (pending.action.includes("install") || pending.action.includes("generate")) {
97
+ return `${pending.action} on ${location}`;
98
+ }
99
+ return `install stable diffusion on ${location}`;
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * Check if user input is an affirmation to execute a pending action.
106
+ */
107
+ export function isAffirmation(text) {
108
+ const normalized = text.toLowerCase().trim();
109
+ const affirmations = [
110
+ "ok", "okay", "yes", "yeah", "yep", "sure", "go", "go ahead",
111
+ "do it", "run it", "try it", "try that", "run that", "do that",
112
+ "execute it", "execute that", "start it", "start that",
113
+ "ok do it", "ok run it", "ok try it", "yes do it", "yes run it",
114
+ "ok go", "go for it", "let's do it", "lets do it", "lets go",
115
+ "proceed", "continue", "confirm", "approve", "yea",
116
+ "ok try that", "ok run that", "ok do that",
117
+ "sure thing", "sounds good", "go on",
118
+ ];
119
+ return affirmations.includes(normalized);
120
+ }
121
+ /**
122
+ * Clear all pending actions.
123
+ */
124
+ export function clearPendingActions() {
125
+ pendingActions = [];
126
+ saveToDisk();
127
+ }