alvin-bot 4.4.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.
- package/.env.example +43 -0
- package/BACKLOG.md +223 -0
- package/CHANGELOG.md +63 -0
- package/CLAUDE.example.md +152 -0
- package/CODE_OF_CONDUCT.md +52 -0
- package/CONTRIBUTING.md +72 -0
- package/LICENSE +21 -0
- package/README.md +529 -0
- package/SECURITY.md +38 -0
- package/SOUL.example.md +60 -0
- package/TOOLS.example.md +42 -0
- package/alvin-bot.config.example.json +24 -0
- package/bin/cli.js +1088 -0
- package/dist/.metadata_never_index +0 -0
- package/dist/claude.js +102 -0
- package/dist/config.js +65 -0
- package/dist/engine.js +90 -0
- package/dist/find-claude-binary.js +98 -0
- package/dist/handlers/commands.js +1489 -0
- package/dist/handlers/document.js +187 -0
- package/dist/handlers/message.js +200 -0
- package/dist/handlers/photo.js +154 -0
- package/dist/handlers/platform-message.js +275 -0
- package/dist/handlers/video.js +237 -0
- package/dist/handlers/voice.js +148 -0
- package/dist/i18n.js +299 -0
- package/dist/index.js +442 -0
- package/dist/init-data-dir.js +81 -0
- package/dist/middleware/auth.js +215 -0
- package/dist/migrate.js +139 -0
- package/dist/paths.js +87 -0
- package/dist/platforms/discord.js +161 -0
- package/dist/platforms/index.js +130 -0
- package/dist/platforms/signal.js +205 -0
- package/dist/platforms/slack.js +318 -0
- package/dist/platforms/telegram.js +111 -0
- package/dist/platforms/types.js +8 -0
- package/dist/platforms/whatsapp.js +648 -0
- package/dist/providers/claude-sdk-provider.js +173 -0
- package/dist/providers/codex-cli-provider.js +121 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/openai-compatible.js +388 -0
- package/dist/providers/registry.js +209 -0
- package/dist/providers/tool-executor.js +450 -0
- package/dist/providers/types.js +205 -0
- package/dist/services/access.js +144 -0
- package/dist/services/asset-index.js +230 -0
- package/dist/services/browser-manager.js +161 -0
- package/dist/services/browser.js +121 -0
- package/dist/services/compaction.js +129 -0
- package/dist/services/cron.js +462 -0
- package/dist/services/custom-tools.js +317 -0
- package/dist/services/delivery-queue.js +154 -0
- package/dist/services/elevenlabs.js +58 -0
- package/dist/services/embeddings.js +386 -0
- package/dist/services/exec-guard.js +46 -0
- package/dist/services/fallback-order.js +151 -0
- package/dist/services/heartbeat.js +192 -0
- package/dist/services/hooks.js +44 -0
- package/dist/services/imagegen.js +72 -0
- package/dist/services/language-detect.js +144 -0
- package/dist/services/markdown.js +63 -0
- package/dist/services/mcp.js +252 -0
- package/dist/services/memory.js +133 -0
- package/dist/services/personality.js +227 -0
- package/dist/services/plugins.js +171 -0
- package/dist/services/reminders.js +97 -0
- package/dist/services/restart.js +48 -0
- package/dist/services/security-audit.js +66 -0
- package/dist/services/self-search.js +129 -0
- package/dist/services/session.js +93 -0
- package/dist/services/skills.js +287 -0
- package/dist/services/standing-orders.js +29 -0
- package/dist/services/subagents.js +142 -0
- package/dist/services/sudo.js +243 -0
- package/dist/services/telegram.js +113 -0
- package/dist/services/tool-discovery.js +214 -0
- package/dist/services/usage-tracker.js +137 -0
- package/dist/services/users.js +199 -0
- package/dist/services/voice.js +95 -0
- package/dist/tui/index.js +507 -0
- package/dist/web/canvas.js +30 -0
- package/dist/web/doctor-api.js +606 -0
- package/dist/web/openai-compat.js +252 -0
- package/dist/web/server.js +1351 -0
- package/dist/web/setup-api.js +1078 -0
- package/docs/mcp.example.json +16 -0
- package/docs/screenshots/00-Login.png +0 -0
- package/docs/screenshots/01-Chat-Dark-Conversation.png +0 -0
- package/docs/screenshots/02-Chat.png +0 -0
- package/docs/screenshots/03-Dashboard-Overview.png +0 -0
- package/docs/screenshots/04-AI-Models-and-Providers.png +0 -0
- package/docs/screenshots/05-Personality-Editor.png +0 -0
- package/docs/screenshots/06-Memory-Manager.png +0 -0
- package/docs/screenshots/07-Active-Sessions.png +0 -0
- package/docs/screenshots/08-File-Browser.png +0 -0
- package/docs/screenshots/09-Scheduled-Jobs.png +0 -0
- package/docs/screenshots/10-Custom-Tools.png +0 -0
- package/docs/screenshots/11-Plugins-and-MCP.png +0 -0
- package/docs/screenshots/12-Messaging-Platforms.png +0 -0
- package/docs/screenshots/12.1-Messaging-Platforms-WhatsApp-Groups-List.png +0 -0
- package/docs/screenshots/12.2-Messaging-Platforms-WA-Group-Details.png +0 -0
- package/docs/screenshots/13-User-Management.png +0 -0
- package/docs/screenshots/14-Web-Terminal.png +0 -0
- package/docs/screenshots/15-Maintenance-and-Health.png +0 -0
- package/docs/screenshots/16-Settings-and-Env.png +0 -0
- package/docs/screenshots/TG-commands.png +0 -0
- package/docs/screenshots/TG.png +0 -0
- package/docs/screenshots/_Mac-Installer.png +0 -0
- package/docs/tools.example.json +33 -0
- package/install.sh +165 -0
- package/package.json +190 -0
- package/plugins/calendar/index.js +270 -0
- package/plugins/email/index.js +231 -0
- package/plugins/finance/index.js +254 -0
- package/plugins/notes/index.js +227 -0
- package/plugins/smarthome/index.js +230 -0
- package/plugins/weather/index.js +122 -0
- package/skills/apple-notes/SKILL.md +31 -0
- package/skills/browse/SKILL.md +136 -0
- package/skills/code-project/SKILL.md +43 -0
- package/skills/data-analysis/SKILL.md +39 -0
- package/skills/document-creation/SKILL.md +48 -0
- package/skills/email-summary/SKILL.md +46 -0
- package/skills/github/SKILL.md +42 -0
- package/skills/summarize/SKILL.md +28 -0
- package/skills/system-admin/SKILL.md +39 -0
- package/skills/weather/SKILL.md +34 -0
- package/skills/web-research/SKILL.md +35 -0
- package/web/public/canvas.html +52 -0
- package/web/public/css/style.css +555 -0
- package/web/public/index.html +189 -0
- package/web/public/js/app.js +3102 -0
- package/web/public/js/i18n.js +1048 -0
- package/web/public/js/icons.js +104 -0
- package/web/public/login.html +48 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finance Plugin — Stock prices, crypto, and currency conversion.
|
|
3
|
+
*
|
|
4
|
+
* Uses free APIs (no key needed):
|
|
5
|
+
* - Yahoo Finance (via query2.finance.yahoo.com)
|
|
6
|
+
* - CoinGecko (crypto)
|
|
7
|
+
* - frankfurter.app (currency conversion)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const YAHOO_BASE = "https://query2.finance.yahoo.com/v8/finance/chart";
|
|
11
|
+
const COINGECKO_BASE = "https://api.coingecko.com/api/v3";
|
|
12
|
+
const CURRENCY_BASE = "https://api.frankfurter.app";
|
|
13
|
+
|
|
14
|
+
// Common crypto IDs
|
|
15
|
+
const CRYPTO_MAP = {
|
|
16
|
+
btc: "bitcoin", bitcoin: "bitcoin",
|
|
17
|
+
eth: "ethereum", ethereum: "ethereum",
|
|
18
|
+
sol: "solana", solana: "solana",
|
|
19
|
+
ada: "cardano", cardano: "cardano",
|
|
20
|
+
doge: "dogecoin", dogecoin: "dogecoin",
|
|
21
|
+
xrp: "ripple", ripple: "ripple",
|
|
22
|
+
dot: "polkadot", polkadot: "polkadot",
|
|
23
|
+
matic: "matic-network", polygon: "matic-network",
|
|
24
|
+
link: "chainlink", chainlink: "chainlink",
|
|
25
|
+
avax: "avalanche-2", avalanche: "avalanche-2",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function fetchJSON(url, headers = {}) {
|
|
29
|
+
const res = await fetch(url, {
|
|
30
|
+
headers: { "User-Agent": "AlvinBot/1.0", ...headers },
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
33
|
+
return res.json();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getStockPrice(symbol) {
|
|
37
|
+
const data = await fetchJSON(`${YAHOO_BASE}/${encodeURIComponent(symbol)}?interval=1d&range=5d`);
|
|
38
|
+
const result = data?.chart?.result?.[0];
|
|
39
|
+
if (!result) throw new Error(`Symbol "${symbol}" not found`);
|
|
40
|
+
|
|
41
|
+
const meta = result.meta;
|
|
42
|
+
const price = meta.regularMarketPrice;
|
|
43
|
+
const prevClose = meta.chartPreviousClose || meta.previousClose;
|
|
44
|
+
const change = price - prevClose;
|
|
45
|
+
const changePct = (change / prevClose * 100);
|
|
46
|
+
const currency = meta.currency || "USD";
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
symbol: meta.symbol,
|
|
50
|
+
name: meta.shortName || meta.symbol,
|
|
51
|
+
price,
|
|
52
|
+
change,
|
|
53
|
+
changePct,
|
|
54
|
+
currency,
|
|
55
|
+
exchange: meta.exchangeName,
|
|
56
|
+
marketState: meta.marketState,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getCryptoPrice(id) {
|
|
61
|
+
const data = await fetchJSON(
|
|
62
|
+
`${COINGECKO_BASE}/simple/price?ids=${id}&vs_currencies=usd,eur&include_24hr_change=true&include_market_cap=true`
|
|
63
|
+
);
|
|
64
|
+
const coin = data[id];
|
|
65
|
+
if (!coin) throw new Error(`Crypto "${id}" not found`);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
usd: coin.usd,
|
|
70
|
+
eur: coin.eur,
|
|
71
|
+
change24h: coin.usd_24h_change,
|
|
72
|
+
marketCapUsd: coin.usd_market_cap,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function convertCurrency(amount, from, to) {
|
|
77
|
+
const data = await fetchJSON(
|
|
78
|
+
`${CURRENCY_BASE}/latest?amount=${amount}&from=${from.toUpperCase()}&to=${to.toUpperCase()}`
|
|
79
|
+
);
|
|
80
|
+
return {
|
|
81
|
+
amount,
|
|
82
|
+
from: from.toUpperCase(),
|
|
83
|
+
to: to.toUpperCase(),
|
|
84
|
+
result: data.rates?.[to.toUpperCase()],
|
|
85
|
+
date: data.date,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatNumber(n, decimals = 2) {
|
|
90
|
+
if (n >= 1e12) return (n / 1e12).toFixed(1) + "T";
|
|
91
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
|
|
92
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
|
93
|
+
return n.toLocaleString("de-DE", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default {
|
|
97
|
+
name: "finance",
|
|
98
|
+
description: "Stock prices, crypto prices and currency conversion",
|
|
99
|
+
version: "1.0.0",
|
|
100
|
+
author: "Alvin Bot",
|
|
101
|
+
|
|
102
|
+
commands: [
|
|
103
|
+
{
|
|
104
|
+
command: "stock",
|
|
105
|
+
description: "Get stock price (e.g. /stock AAPL)",
|
|
106
|
+
handler: async (ctx, args) => {
|
|
107
|
+
if (!args) {
|
|
108
|
+
await ctx.reply("📈 Use: `/stock AAPL` or `/stock MSFT GOOGL TSLA`", { parse_mode: "Markdown" });
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const symbols = args.toUpperCase().split(/[\s,]+/).filter(Boolean).slice(0, 5);
|
|
113
|
+
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
|
114
|
+
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const sym of symbols) {
|
|
117
|
+
try {
|
|
118
|
+
const data = await getStockPrice(sym);
|
|
119
|
+
const arrow = data.change >= 0 ? "📈" : "📉";
|
|
120
|
+
const sign = data.change >= 0 ? "+" : "";
|
|
121
|
+
results.push(
|
|
122
|
+
`${arrow} *${data.symbol}* (${data.name})\n` +
|
|
123
|
+
` ${formatNumber(data.price)} ${data.currency} (${sign}${formatNumber(data.change)} / ${sign}${data.changePct.toFixed(2)}%)\n` +
|
|
124
|
+
` _${data.exchange} — ${data.marketState}_`
|
|
125
|
+
);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
results.push(`❌ ${sym}: ${err.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await ctx.reply(results.join("\n\n"), { parse_mode: "Markdown" });
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
command: "crypto",
|
|
136
|
+
description: "Get crypto price (e.g. /crypto btc)",
|
|
137
|
+
handler: async (ctx, args) => {
|
|
138
|
+
if (!args) {
|
|
139
|
+
await ctx.reply("🪙 Use: `/crypto btc` or `/crypto eth sol doge`", { parse_mode: "Markdown" });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const coins = args.toLowerCase().split(/[\s,]+/).filter(Boolean).slice(0, 5);
|
|
144
|
+
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
|
145
|
+
|
|
146
|
+
const results = [];
|
|
147
|
+
for (const coin of coins) {
|
|
148
|
+
const id = CRYPTO_MAP[coin] || coin;
|
|
149
|
+
try {
|
|
150
|
+
const data = await getCryptoPrice(id);
|
|
151
|
+
const arrow = data.change24h >= 0 ? "📈" : "📉";
|
|
152
|
+
const sign = data.change24h >= 0 ? "+" : "";
|
|
153
|
+
results.push(
|
|
154
|
+
`${arrow} *${id.charAt(0).toUpperCase() + id.slice(1)}*\n` +
|
|
155
|
+
` $${formatNumber(data.usd)} / €${formatNumber(data.eur)}\n` +
|
|
156
|
+
` 24h: ${sign}${data.change24h?.toFixed(2)}% | MCap: $${formatNumber(data.marketCapUsd, 0)}`
|
|
157
|
+
);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
results.push(`❌ ${coin}: ${err.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await ctx.reply(results.join("\n\n"), { parse_mode: "Markdown" });
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
command: "fx",
|
|
168
|
+
description: "Convert currency (e.g. /fx 100 USD EUR)",
|
|
169
|
+
handler: async (ctx, args) => {
|
|
170
|
+
if (!args) {
|
|
171
|
+
await ctx.reply("💱 Use: `/fx 100 USD EUR`", { parse_mode: "Markdown" });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const parts = args.split(/\s+/);
|
|
176
|
+
if (parts.length < 3) {
|
|
177
|
+
await ctx.reply("Format: `/fx <amount> <FROM> <TO>`\nExample: `/fx 100 USD EUR`", { parse_mode: "Markdown" });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const amount = parseFloat(parts[0]);
|
|
182
|
+
if (isNaN(amount)) {
|
|
183
|
+
await ctx.reply("❌ Invalid amount.");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await ctx.api.sendChatAction(ctx.chat.id, "typing");
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const data = await convertCurrency(amount, parts[1], parts[2]);
|
|
191
|
+
if (!data.result) throw new Error("Currency pair not supported");
|
|
192
|
+
await ctx.reply(
|
|
193
|
+
`💱 *${formatNumber(data.amount)} ${data.from}* = *${formatNumber(data.result)} ${data.to}*\n` +
|
|
194
|
+
`_Rate from ${data.date}_`,
|
|
195
|
+
{ parse_mode: "Markdown" }
|
|
196
|
+
);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
await ctx.reply(`❌ ${err.message}`);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
|
|
204
|
+
tools: [
|
|
205
|
+
{
|
|
206
|
+
name: "get_stock_price",
|
|
207
|
+
description: "Get current stock price for a ticker symbol",
|
|
208
|
+
parameters: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
symbol: { type: "string", description: "Ticker symbol (e.g. AAPL, MSFT)" },
|
|
212
|
+
},
|
|
213
|
+
required: ["symbol"],
|
|
214
|
+
},
|
|
215
|
+
execute: async (params) => {
|
|
216
|
+
const data = await getStockPrice(params.symbol);
|
|
217
|
+
return JSON.stringify(data);
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: "get_crypto_price",
|
|
222
|
+
description: "Get current cryptocurrency price",
|
|
223
|
+
parameters: {
|
|
224
|
+
type: "object",
|
|
225
|
+
properties: {
|
|
226
|
+
coin: { type: "string", description: "Coin name or symbol (e.g. bitcoin, btc, eth)" },
|
|
227
|
+
},
|
|
228
|
+
required: ["coin"],
|
|
229
|
+
},
|
|
230
|
+
execute: async (params) => {
|
|
231
|
+
const id = CRYPTO_MAP[params.coin?.toLowerCase()] || params.coin;
|
|
232
|
+
const data = await getCryptoPrice(id);
|
|
233
|
+
return JSON.stringify(data);
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: "convert_currency",
|
|
238
|
+
description: "Convert between currencies",
|
|
239
|
+
parameters: {
|
|
240
|
+
type: "object",
|
|
241
|
+
properties: {
|
|
242
|
+
amount: { type: "number", description: "Amount to convert" },
|
|
243
|
+
from: { type: "string", description: "Source currency (e.g. USD)" },
|
|
244
|
+
to: { type: "string", description: "Target currency (e.g. EUR)" },
|
|
245
|
+
},
|
|
246
|
+
required: ["amount", "from", "to"],
|
|
247
|
+
},
|
|
248
|
+
execute: async (params) => {
|
|
249
|
+
const data = await convertCurrency(params.amount, params.from, params.to);
|
|
250
|
+
return JSON.stringify(data);
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notes Plugin — Simple markdown notes with search.
|
|
3
|
+
*
|
|
4
|
+
* Stores notes in docs/notes/ as markdown files.
|
|
5
|
+
* No external dependencies — pure filesystem.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
|
|
12
|
+
const PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
13
|
+
const NOTES_DIR = path.resolve(PLUGIN_ROOT, "docs", "notes");
|
|
14
|
+
|
|
15
|
+
// Ensure dir exists
|
|
16
|
+
if (!fs.existsSync(NOTES_DIR)) fs.mkdirSync(NOTES_DIR, { recursive: true });
|
|
17
|
+
|
|
18
|
+
function slugify(text) {
|
|
19
|
+
return text
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[äöüß]/g, c => ({ ä: "ae", ö: "oe", ü: "ue", ß: "ss" }[c] || c))
|
|
22
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
23
|
+
.replace(/(^-|-$)/g, "")
|
|
24
|
+
.slice(0, 60);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function listNotes() {
|
|
28
|
+
try {
|
|
29
|
+
return fs.readdirSync(NOTES_DIR)
|
|
30
|
+
.filter(f => f.endsWith(".md"))
|
|
31
|
+
.map(f => {
|
|
32
|
+
const content = fs.readFileSync(path.resolve(NOTES_DIR, f), "utf-8");
|
|
33
|
+
const title = content.split("\n")[0]?.replace(/^#\s*/, "") || f;
|
|
34
|
+
const stat = fs.statSync(path.resolve(NOTES_DIR, f));
|
|
35
|
+
return { filename: f, title, size: stat.size, modified: stat.mtimeMs };
|
|
36
|
+
})
|
|
37
|
+
.sort((a, b) => b.modified - a.modified);
|
|
38
|
+
} catch { return []; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function searchNotes(query) {
|
|
42
|
+
const q = query.toLowerCase();
|
|
43
|
+
return listNotes().filter(n => {
|
|
44
|
+
const content = fs.readFileSync(path.resolve(NOTES_DIR, n.filename), "utf-8").toLowerCase();
|
|
45
|
+
return content.includes(q) || n.title.toLowerCase().includes(q);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
name: "notes",
|
|
51
|
+
description: "Create, read and search markdown notes",
|
|
52
|
+
version: "1.0.0",
|
|
53
|
+
author: "Alvin Bot",
|
|
54
|
+
|
|
55
|
+
commands: [
|
|
56
|
+
{
|
|
57
|
+
command: "notes",
|
|
58
|
+
description: "Manage notes",
|
|
59
|
+
handler: async (ctx, args) => {
|
|
60
|
+
// /notes — list all
|
|
61
|
+
if (!args) {
|
|
62
|
+
const notes = listNotes();
|
|
63
|
+
if (notes.length === 0) {
|
|
64
|
+
await ctx.reply("📝 No notes yet.\nCreate one with `/notes add <Title> | <Content>`", { parse_mode: "Markdown" });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const lines = notes.slice(0, 20).map((n, i) => {
|
|
69
|
+
const date = new Date(n.modified).toLocaleDateString("de-DE");
|
|
70
|
+
return `${i + 1}. *${n.title}* (${date})`;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await ctx.reply(`📝 *Notes (${notes.length}):*\n\n${lines.join("\n")}`, { parse_mode: "Markdown" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// /notes add <title> | <content>
|
|
78
|
+
if (args.startsWith("add ")) {
|
|
79
|
+
const text = args.slice(4).trim();
|
|
80
|
+
const pipeIdx = text.indexOf("|");
|
|
81
|
+
let title, content;
|
|
82
|
+
|
|
83
|
+
if (pipeIdx > 0) {
|
|
84
|
+
title = text.slice(0, pipeIdx).trim();
|
|
85
|
+
content = text.slice(pipeIdx + 1).trim();
|
|
86
|
+
} else {
|
|
87
|
+
title = text.slice(0, 50).trim();
|
|
88
|
+
content = text;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const slug = slugify(title);
|
|
92
|
+
const filename = `${slug}.md`;
|
|
93
|
+
const filePath = path.resolve(NOTES_DIR, filename);
|
|
94
|
+
|
|
95
|
+
const md = `# ${title}\n\n${content}\n\n---\n_Created: ${new Date().toLocaleString("de-DE")}_\n`;
|
|
96
|
+
fs.writeFileSync(filePath, md);
|
|
97
|
+
|
|
98
|
+
await ctx.reply(`✅ Note saved: *${title}*`, { parse_mode: "Markdown" });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// /notes search <query>
|
|
103
|
+
if (args.startsWith("search ")) {
|
|
104
|
+
const query = args.slice(7).trim();
|
|
105
|
+
if (!query) {
|
|
106
|
+
await ctx.reply("Format: `/notes search <query>`", { parse_mode: "Markdown" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const results = searchNotes(query);
|
|
111
|
+
if (results.length === 0) {
|
|
112
|
+
await ctx.reply(`🔍 No notes found for "${query}".`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const lines = results.slice(0, 10).map((n, i) => `${i + 1}. *${n.title}*`);
|
|
117
|
+
await ctx.reply(`🔍 *${results.length} results for "${query}":*\n\n${lines.join("\n")}`, { parse_mode: "Markdown" });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// /notes view <number or title>
|
|
122
|
+
if (args.startsWith("view ") || args.startsWith("read ")) {
|
|
123
|
+
const query = args.split(" ").slice(1).join(" ").trim();
|
|
124
|
+
const notes = listNotes();
|
|
125
|
+
const idx = parseInt(query) - 1;
|
|
126
|
+
let note;
|
|
127
|
+
|
|
128
|
+
if (!isNaN(idx) && idx >= 0 && idx < notes.length) {
|
|
129
|
+
note = notes[idx];
|
|
130
|
+
} else {
|
|
131
|
+
note = notes.find(n => n.title.toLowerCase().includes(query.toLowerCase()));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!note) {
|
|
135
|
+
await ctx.reply(`❌ Note "${query}" not found.`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const content = fs.readFileSync(path.resolve(NOTES_DIR, note.filename), "utf-8");
|
|
140
|
+
const truncated = content.length > 3000 ? content.slice(0, 3000) + "\n\n_[...truncated]_" : content;
|
|
141
|
+
await ctx.reply(truncated, { parse_mode: "Markdown" });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// /notes delete <number>
|
|
146
|
+
if (args.startsWith("delete ") || args.startsWith("del ")) {
|
|
147
|
+
const query = args.split(" ").slice(1).join(" ").trim();
|
|
148
|
+
const notes = listNotes();
|
|
149
|
+
const idx = parseInt(query) - 1;
|
|
150
|
+
let note;
|
|
151
|
+
|
|
152
|
+
if (!isNaN(idx) && idx >= 0 && idx < notes.length) {
|
|
153
|
+
note = notes[idx];
|
|
154
|
+
} else {
|
|
155
|
+
note = notes.find(n => n.title.toLowerCase().includes(query.toLowerCase()));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!note) {
|
|
159
|
+
await ctx.reply(`❌ Note "${query}" not found.`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fs.unlinkSync(path.resolve(NOTES_DIR, note.filename));
|
|
164
|
+
await ctx.reply(`🗑️ Note deleted: *${note.title}*`, { parse_mode: "Markdown" });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await ctx.reply(
|
|
169
|
+
"📝 *Notes commands:*\n\n" +
|
|
170
|
+
"`/notes` — List all\n" +
|
|
171
|
+
"`/notes add Title | Content` — Create\n" +
|
|
172
|
+
"`/notes view 1` — Read (number or title)\n" +
|
|
173
|
+
"`/notes search query` — Search\n" +
|
|
174
|
+
"`/notes delete 1` — Delete",
|
|
175
|
+
{ parse_mode: "Markdown" }
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
|
|
181
|
+
tools: [
|
|
182
|
+
{
|
|
183
|
+
name: "create_note",
|
|
184
|
+
description: "Create a markdown note",
|
|
185
|
+
parameters: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
title: { type: "string", description: "Note title" },
|
|
189
|
+
content: { type: "string", description: "Note content (markdown)" },
|
|
190
|
+
},
|
|
191
|
+
required: ["title", "content"],
|
|
192
|
+
},
|
|
193
|
+
execute: async (params) => {
|
|
194
|
+
const slug = slugify(params.title);
|
|
195
|
+
const filename = `${slug}.md`;
|
|
196
|
+
const filePath = path.resolve(NOTES_DIR, filename);
|
|
197
|
+
const md = `# ${params.title}\n\n${params.content}\n\n---\n_Created: ${new Date().toLocaleString("de-DE")}_\n`;
|
|
198
|
+
fs.writeFileSync(filePath, md);
|
|
199
|
+
return `Note saved: ${filename}`;
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "search_notes",
|
|
204
|
+
description: "Search through notes",
|
|
205
|
+
parameters: {
|
|
206
|
+
type: "object",
|
|
207
|
+
properties: {
|
|
208
|
+
query: { type: "string", description: "Search query" },
|
|
209
|
+
},
|
|
210
|
+
required: ["query"],
|
|
211
|
+
},
|
|
212
|
+
execute: async (params) => {
|
|
213
|
+
const results = searchNotes(params.query);
|
|
214
|
+
return JSON.stringify(results.map(r => ({ title: r.title, filename: r.filename })));
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "list_notes",
|
|
219
|
+
description: "List all notes",
|
|
220
|
+
parameters: { type: "object", properties: {} },
|
|
221
|
+
execute: async () => {
|
|
222
|
+
const notes = listNotes();
|
|
223
|
+
return JSON.stringify(notes.map(n => ({ title: n.title, modified: new Date(n.modified).toISOString() })));
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
};
|