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.
- package/config/chat-responses.json +767 -0
- package/config/concept-clusters.json +31 -0
- package/config/entities.json +93 -0
- package/config/image-prompts.json +20 -0
- package/config/intent-vectors.json +1 -0
- package/config/intents.json +5023 -65
- package/config/ollama-models.json +193 -0
- package/config/rules.json +32 -1
- package/dist/automation/discordPatchright.d.ts +35 -0
- package/dist/automation/discordPatchright.js +424 -0
- package/dist/automation/discordSetup.d.ts +31 -0
- package/dist/automation/discordSetup.js +338 -0
- package/dist/conversation/coreference.js +44 -4
- package/dist/conversation/pendingActions.d.ts +55 -0
- package/dist/conversation/pendingActions.js +127 -0
- package/dist/conversation/store.d.ts +72 -0
- package/dist/conversation/store.js +140 -1
- package/dist/conversation/topicTracker.d.ts +36 -0
- package/dist/conversation/topicTracker.js +141 -0
- package/dist/execution/ssh.d.ts +42 -1
- package/dist/execution/ssh.js +532 -3
- package/dist/handlers/executor.js +3981 -16
- package/dist/index.d.ts +25 -3
- package/dist/index.js +36 -2
- package/dist/nlp/batchParser.d.ts +30 -0
- package/dist/nlp/batchParser.js +77 -0
- package/dist/nlp/conceptExpansion.d.ts +54 -0
- package/dist/nlp/conceptExpansion.js +136 -0
- package/dist/nlp/conceptRouter.d.ts +49 -0
- package/dist/nlp/conceptRouter.js +302 -0
- package/dist/nlp/confidenceCalibrator.d.ts +62 -0
- package/dist/nlp/confidenceCalibrator.js +116 -0
- package/dist/nlp/correctionLearner.d.ts +45 -0
- package/dist/nlp/correctionLearner.js +207 -0
- package/dist/nlp/entitySpellCorrect.d.ts +35 -0
- package/dist/nlp/entitySpellCorrect.js +141 -0
- package/dist/nlp/knowledgeGraph.d.ts +70 -0
- package/dist/nlp/knowledgeGraph.js +380 -0
- package/dist/nlp/llmFallback.js +28 -1
- package/dist/nlp/multiClassifier.js +91 -6
- package/dist/nlp/multiIntent.d.ts +43 -0
- package/dist/nlp/multiIntent.js +154 -0
- package/dist/nlp/parseIntent.d.ts +6 -1
- package/dist/nlp/parseIntent.js +180 -5
- package/dist/nlp/ruleParser.js +315 -0
- package/dist/nlp/semanticSimilarity.d.ts +30 -0
- package/dist/nlp/semanticSimilarity.js +174 -0
- package/dist/nlp/vocabularyBuilder.d.ts +43 -0
- package/dist/nlp/vocabularyBuilder.js +224 -0
- package/dist/nlp/wikidata.d.ts +49 -0
- package/dist/nlp/wikidata.js +228 -0
- package/dist/policy/confirm.d.ts +10 -0
- package/dist/policy/confirm.js +39 -0
- package/dist/policy/safety.js +6 -4
- package/dist/utils/aliases.d.ts +5 -0
- package/dist/utils/aliases.js +39 -0
- package/dist/utils/analysis.js +71 -15
- package/dist/utils/browser.d.ts +64 -0
- package/dist/utils/browser.js +364 -0
- package/dist/utils/commandHistory.d.ts +20 -0
- package/dist/utils/commandHistory.js +108 -0
- package/dist/utils/completer.d.ts +17 -0
- package/dist/utils/completer.js +79 -0
- package/dist/utils/config.js +32 -2
- package/dist/utils/dbQuery.d.ts +25 -0
- package/dist/utils/dbQuery.js +248 -0
- package/dist/utils/discordDiag.d.ts +35 -0
- package/dist/utils/discordDiag.js +826 -0
- package/dist/utils/diskCleanup.d.ts +36 -0
- package/dist/utils/diskCleanup.js +775 -0
- package/dist/utils/entityResolver.d.ts +107 -0
- package/dist/utils/entityResolver.js +468 -0
- package/dist/utils/imageGen.d.ts +92 -0
- package/dist/utils/imageGen.js +2031 -0
- package/dist/utils/installTracker.d.ts +57 -0
- package/dist/utils/installTracker.js +160 -0
- package/dist/utils/multiExec.d.ts +21 -0
- package/dist/utils/multiExec.js +141 -0
- package/dist/utils/openclawDiag.d.ts +29 -0
- package/dist/utils/openclawDiag.js +1035 -0
- package/dist/utils/output.js +4 -0
- package/dist/utils/platform.js +2 -1
- package/dist/utils/progressReporter.d.ts +50 -0
- package/dist/utils/progressReporter.js +58 -0
- package/dist/utils/projectDetect.d.ts +44 -0
- package/dist/utils/projectDetect.js +319 -0
- package/dist/utils/projectScanner.d.ts +44 -0
- package/dist/utils/projectScanner.js +312 -0
- package/dist/utils/shellCompat.d.ts +78 -0
- package/dist/utils/shellCompat.js +186 -0
- package/dist/utils/smartArchive.d.ts +16 -0
- package/dist/utils/smartArchive.js +172 -0
- package/dist/utils/smartRetry.d.ts +26 -0
- package/dist/utils/smartRetry.js +114 -0
- package/dist/utils/updater.d.ts +1 -0
- package/dist/utils/updater.js +1 -1
- package/dist/utils/version.d.ts +20 -0
- package/dist/utils/version.js +212 -0
- package/package.json +6 -3
|
@@ -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 (
|
|
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
|
+
}
|