clawlet 0.1.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/src/cli.ts ADDED
@@ -0,0 +1,189 @@
1
+ import * as readline from 'readline';
2
+ import 'dotenv/config';
3
+ import { Agent, type InputAdapter, type OutputAdapter } from './agent.js';
4
+ import { Bot } from 'grammy';
5
+
6
+ // --- CLI Input Adapter ---
7
+
8
+ class CliInput implements InputAdapter {
9
+ private handler!: (text: string, label: string) => void;
10
+ private rl: readline.Interface;
11
+
12
+ constructor(rl: readline.Interface) {
13
+ this.rl = rl;
14
+ }
15
+
16
+ onMessage(handler: (text: string, label: string) => void) {
17
+ this.handler = handler;
18
+ }
19
+
20
+ start() {
21
+ this.rl.on('line', (input) => {
22
+ const trimmed = input.trim();
23
+ if (trimmed === 'exit') process.exit(0);
24
+ if (!trimmed) { this.rl.prompt(); return; }
25
+ this.handler(trimmed, 'cli');
26
+ });
27
+ this.rl.prompt();
28
+ }
29
+ }
30
+
31
+ // --- CLI Output Adapter ---
32
+
33
+ class CliOutput implements OutputAdapter {
34
+ constructor(private rl: readline.Interface) {}
35
+
36
+ onAgentStart(label: string) {
37
+ if (label !== 'cli') {
38
+ readline.clearLine(process.stdout, 0);
39
+ readline.cursorTo(process.stdout, 0);
40
+ // The label from telegram input includes the message text via logging in TelegramInput
41
+ }
42
+ process.stdout.write("Agent: ");
43
+ }
44
+
45
+ onResponseChunk(chunk: string) {
46
+ process.stdout.write(chunk);
47
+ }
48
+
49
+ onResponseEnd(_fullResponse: string) {
50
+ console.log();
51
+ this.rl.prompt();
52
+ }
53
+
54
+ onError(error: Error) {
55
+ console.error("\nāŒ Error:", error.message);
56
+ this.rl.prompt();
57
+ }
58
+ }
59
+
60
+ // --- Telegram Input Adapter ---
61
+
62
+ class TelegramInput implements InputAdapter {
63
+ private handler!: (text: string, label: string) => void;
64
+ private bot: Bot;
65
+ private authorizedChatId: string | undefined;
66
+ /** Shared set so the output adapter knows which chats to send to */
67
+ private activeChatIds: Set<number>;
68
+
69
+ constructor(bot: Bot, activeChatIds: Set<number>, authorizedChatId?: string) {
70
+ this.bot = bot;
71
+ this.activeChatIds = activeChatIds;
72
+ this.authorizedChatId = authorizedChatId;
73
+ }
74
+
75
+ onMessage(handler: (text: string, label: string) => void) {
76
+ this.handler = handler;
77
+ }
78
+
79
+ start() {
80
+ this.bot.command("start", (ctx) => ctx.reply("Clawlet is online."));
81
+
82
+ this.bot.on("message:text", async (ctx) => {
83
+ const chatId = ctx.chat.id;
84
+
85
+ if (this.authorizedChatId && chatId.toString() !== this.authorizedChatId) {
86
+ console.log(`Unauthorized access attempt from Telegram chat ID: ${chatId}`);
87
+ return;
88
+ }
89
+
90
+ this.activeChatIds.add(chatId);
91
+ console.log(`\n[Telegram] ${ctx.message.text}`);
92
+ this.handler(ctx.message.text, 'telegram');
93
+ });
94
+
95
+ this.bot.start();
96
+ console.log("šŸ¤– Telegram Bot started.");
97
+ }
98
+ }
99
+
100
+ // --- Telegram Output Adapter ---
101
+
102
+ class TelegramOutput implements OutputAdapter {
103
+ private bot: Bot;
104
+ /** Shared reference to active chat IDs (populated by TelegramInput) */
105
+ private activeChatIds: Set<number>;
106
+ private typingInterval: NodeJS.Timeout | null = null;
107
+
108
+ constructor(bot: Bot, activeChatIds: Set<number>) {
109
+ this.bot = bot;
110
+ this.activeChatIds = activeChatIds;
111
+ }
112
+
113
+ onAgentStart(_label: string) {
114
+ if (this.activeChatIds.size === 0) return;
115
+ for (const chatId of this.activeChatIds) {
116
+ this.bot.api.sendChatAction(chatId, "typing").catch(() => {});
117
+ }
118
+ this.typingInterval = setInterval(() => {
119
+ for (const chatId of this.activeChatIds) {
120
+ this.bot.api.sendChatAction(chatId, "typing").catch(() => {});
121
+ }
122
+ }, 4000);
123
+ }
124
+
125
+ onResponseChunk(_chunk: string) {
126
+ // Telegram doesn't stream — full message sent at end
127
+ }
128
+
129
+ async onResponseEnd(fullResponse: string) {
130
+ this.stopTyping();
131
+ const text = fullResponse.trim() || "āœ… Done.";
132
+ for (const chatId of this.activeChatIds) {
133
+ try {
134
+ if (text.length > 4000) {
135
+ const chunks = text.match(/.{1,4000}/g) || [];
136
+ for (const chunk of chunks) {
137
+ await this.bot.api.sendMessage(chatId, chunk);
138
+ }
139
+ } else {
140
+ await this.bot.api.sendMessage(chatId, text);
141
+ }
142
+ } catch (e: any) {
143
+ console.error(` āš ļø Failed to send to Telegram chat ${chatId}: ${e.message}`);
144
+ }
145
+ }
146
+ }
147
+
148
+ onError(error: Error) {
149
+ this.stopTyping();
150
+ for (const chatId of this.activeChatIds) {
151
+ this.bot.api.sendMessage(chatId, `āš ļø Error: ${error.message}`).catch(() => {});
152
+ }
153
+ }
154
+
155
+ private stopTyping() {
156
+ if (this.typingInterval) {
157
+ clearInterval(this.typingInterval);
158
+ this.typingInterval = null;
159
+ }
160
+ }
161
+ }
162
+
163
+ // --- MAIN ---
164
+
165
+ const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
166
+ const TELEGRAM_USERINFO_ID = process.env.TELEGRAM_USERINFO_ID;
167
+
168
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '\nYou: ' });
169
+ const agent = new Agent();
170
+
171
+ // Always add CLI adapters
172
+ agent.addInput(new CliInput(rl));
173
+ agent.addOutput(new CliOutput(rl));
174
+
175
+ // Add Telegram if configured via .env
176
+ if (TELEGRAM_BOT_TOKEN) {
177
+ const bot = new Bot(TELEGRAM_BOT_TOKEN);
178
+ const activeChatIds = new Set<number>();
179
+
180
+ agent.addInput(new TelegramInput(bot, activeChatIds, TELEGRAM_USERINFO_ID ?? undefined));
181
+ agent.addOutput(new TelegramOutput(bot, activeChatIds));
182
+
183
+ console.log("šŸ¤– Telegram integration enabled.");
184
+ } else {
185
+ console.log("āš ļø TELEGRAM_BOT_TOKEN not found. Running CLI only.");
186
+ }
187
+
188
+ console.log("šŸ¤– Clawlet CLI initialized.");
189
+ await agent.start();
package/src/memory.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { createStorage, type Storage } from "unstorage";
2
+ import fsDriver from "unstorage/drivers/fs";
3
+ import { type ModelMessage } from "ai";
4
+ import path from "path";
5
+ import { LibSqlKeyValueStorage, LibSqlListStorage, SkillHistoryStorage } from "./storage.js";
6
+
7
+ export class AgentMemory {
8
+ // 1. Secrets (libSQL - file:secrets.db)
9
+ public secrets: LibSqlKeyValueStorage;
10
+
11
+ // 2. History (libSQL - file:history.db)
12
+ public history: LibSqlListStorage<ModelMessage>;
13
+
14
+ // 3. Skill History (libSQL - file:history.db, table: skill_history)
15
+ public skillHistory: SkillHistoryStorage<ModelMessage>;
16
+
17
+ // 4. Workspace (Unstorage - ./workspace)
18
+ public workspace: Storage;
19
+
20
+ constructor() {
21
+ // A. Init Secrets DB
22
+ // In Production: process.env.SECRETS_DB_URL (libsql://...)
23
+ this.secrets = new LibSqlKeyValueStorage(
24
+ process.env.SECRETS_DB_URL || "file:secrets.db",
25
+ process.env.SECRETS_AUTH_TOKEN
26
+ );
27
+
28
+ // B. Init History DB
29
+ // In Production: process.env.HISTORY_DB_URL (libsql://...)
30
+ this.history = new LibSqlListStorage<ModelMessage>(
31
+ process.env.HISTORY_DB_URL || "file:history.db",
32
+ process.env.HISTORY_AUTH_TOKEN
33
+ );
34
+
35
+ // C. Init Skill History (same DB as history, different table)
36
+ this.skillHistory = new SkillHistoryStorage<ModelMessage>(
37
+ process.env.HISTORY_DB_URL || "file:history.db",
38
+ process.env.HISTORY_AUTH_TOKEN
39
+ );
40
+
41
+ // D. Init Workspace (Filesystem)
42
+ // Unstorage abstrahiert hier nur das "Wie", aber es bleibt lokal im Ordner.
43
+ this.workspace = createStorage({
44
+ driver: fsDriver({ base: path.join(process.cwd(), "workspace") })
45
+ });
46
+ }
47
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,190 @@
1
+ import { createClient, type Client } from '@libsql/client';
2
+
3
+ // --- A. Key-Value Storage (für Secrets/Config) ---
4
+ export class LibSqlKeyValueStorage {
5
+ private client: Client;
6
+ private tableName: string;
7
+
8
+ constructor(url: string, authToken?: string, tableName = 'kv_store') {
9
+ this.tableName = tableName;
10
+ this.client = createClient({ url, authToken });
11
+ this.init();
12
+ }
13
+
14
+ private async init() {
15
+ await this.client.execute(`
16
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
17
+ key TEXT PRIMARY KEY,
18
+ value TEXT
19
+ )
20
+ `);
21
+ }
22
+
23
+ async set(key: string, value: string) {
24
+ // Upsert (Insert or Replace)
25
+ await this.client.execute({
26
+ sql: `INSERT INTO ${this.tableName} (key, value) VALUES (?, ?)
27
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
28
+ args: [key, value]
29
+ });
30
+ }
31
+
32
+ async get(key: string): Promise<string | null> {
33
+ const rs = await this.client.execute({
34
+ sql: `SELECT value FROM ${this.tableName} WHERE key = ?`,
35
+ args: [key]
36
+ });
37
+ return (rs.rows[0]?.value as string) || null;
38
+ }
39
+
40
+ async has(key: string): Promise<boolean> {
41
+ const rs = await this.client.execute({
42
+ sql: `SELECT 1 FROM ${this.tableName} WHERE key = ? LIMIT 1`,
43
+ args: [key]
44
+ });
45
+ return rs.rows.length > 0;
46
+ }
47
+
48
+ async delete(key: string) {
49
+ await this.client.execute({
50
+ sql: `DELETE FROM ${this.tableName} WHERE key = ?`,
51
+ args: [key]
52
+ });
53
+ }
54
+
55
+ async listKeys(): Promise<string[]> {
56
+ const rs = await this.client.execute(`SELECT key FROM ${this.tableName}`);
57
+ return rs.rows.map(row => row.key as string);
58
+ }
59
+ }
60
+
61
+ // --- B. List Storage (für History/Logs) ---
62
+ export class LibSqlListStorage<T = any> {
63
+ private client: Client;
64
+ private tableName: string;
65
+
66
+ constructor(url: string, authToken?: string, tableName = 'list_store') {
67
+ this.tableName = tableName;
68
+ this.client = createClient({ url, authToken });
69
+ this.init();
70
+ }
71
+
72
+ private async init() {
73
+ await this.client.execute(`
74
+ CREATE TABLE IF NOT EXISTS ${this.tableName} (
75
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
76
+ item TEXT,
77
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
78
+ )
79
+ `);
80
+ }
81
+
82
+ async push(item: T) {
83
+ await this.client.execute({
84
+ sql: `INSERT INTO ${this.tableName} (item) VALUES (?)`,
85
+ args: [JSON.stringify(item)]
86
+ });
87
+ }
88
+
89
+ // Bulk Insert für Performance
90
+ async pushMany(items: T[]) {
91
+ const promises = items.map(item => this.client.execute({
92
+ sql: `INSERT INTO ${this.tableName} (item) VALUES (?)`,
93
+ args: [JSON.stringify(item)]
94
+ }));
95
+ await Promise.all(promises);
96
+ }
97
+
98
+ async getAll(): Promise<T[]> {
99
+ const rs = await this.client.execute(
100
+ `SELECT item FROM ${this.tableName} ORDER BY id ASC`
101
+ );
102
+ return rs.rows.map(row => JSON.parse(row.item as string));
103
+ }
104
+
105
+ // Optional: Nur die letzten N Nachrichten holen (Kontext-Fenster!)
106
+ async getRecent(limit: number): Promise<T[]> {
107
+ // Trick: Erst sortieren DESC (neueste), limitieren, dann wieder ASC sortieren
108
+ const rs = await this.client.execute({
109
+ sql: `SELECT * FROM (
110
+ SELECT item, id FROM ${this.tableName} ORDER BY id DESC LIMIT ?
111
+ ) ORDER BY id ASC`,
112
+ args: [limit]
113
+ });
114
+ return rs.rows.map(row => JSON.parse(row.item as string));
115
+ }
116
+
117
+ async count(): Promise<number> {
118
+ const rs = await this.client.execute(
119
+ `SELECT COUNT(*) as cnt FROM ${this.tableName}`
120
+ );
121
+ return Number(rs.rows[0]?.cnt ?? 0);
122
+ }
123
+
124
+ async clear() {
125
+ await this.client.execute(`DELETE FROM ${this.tableName}`);
126
+ // Reset autoincrement so IDs stay clean
127
+ await this.client.execute(
128
+ `DELETE FROM sqlite_sequence WHERE name = '${this.tableName}'`
129
+ );
130
+ }
131
+ }
132
+
133
+ // --- C. Skill History Storage (single table, partitioned by skill name) ---
134
+ export class SkillHistoryStorage<T = any> {
135
+ private client: Client;
136
+
137
+ constructor(url: string, authToken?: string) {
138
+ this.client = createClient({ url, authToken });
139
+ this.init();
140
+ }
141
+
142
+ private async init() {
143
+ await this.client.execute(`
144
+ CREATE TABLE IF NOT EXISTS skill_history (
145
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
146
+ name TEXT NOT NULL,
147
+ item TEXT,
148
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
149
+ )
150
+ `);
151
+ }
152
+
153
+ async push(name: string, item: T) {
154
+ await this.client.execute({
155
+ sql: `INSERT INTO skill_history (name, item) VALUES (?, ?)`,
156
+ args: [name, JSON.stringify(item)]
157
+ });
158
+ }
159
+
160
+ async pushMany(name: string, items: T[]) {
161
+ const promises = items.map(item => this.client.execute({
162
+ sql: `INSERT INTO skill_history (name, item) VALUES (?, ?)`,
163
+ args: [name, JSON.stringify(item)]
164
+ }));
165
+ await Promise.all(promises);
166
+ }
167
+
168
+ async getAll(name: string): Promise<T[]> {
169
+ const rs = await this.client.execute({
170
+ sql: `SELECT item FROM skill_history WHERE name = ? ORDER BY id ASC`,
171
+ args: [name]
172
+ });
173
+ return rs.rows.map(row => JSON.parse(row.item as string));
174
+ }
175
+
176
+ async clear(name: string) {
177
+ await this.client.execute({
178
+ sql: `DELETE FROM skill_history WHERE name = ?`,
179
+ args: [name]
180
+ });
181
+ }
182
+
183
+ async count(name: string): Promise<number> {
184
+ const rs = await this.client.execute({
185
+ sql: `SELECT COUNT(*) as cnt FROM skill_history WHERE name = ?`,
186
+ args: [name]
187
+ });
188
+ return Number(rs.rows[0]?.cnt ?? 0);
189
+ }
190
+ }
@@ -0,0 +1,122 @@
1
+ # System Identity & Architecture
2
+
3
+ You are an AI agent running on **Qwen3-4B-Instruct**.
4
+ - **Environment:** `mlx_lm.server` (local Apple Silicon execution).
5
+ - **Strengths:** Speed, code generation, logical instruction following.
6
+ - **Constraints:** You have a smaller parameter count than massive frontier models. You must compensate by being **explicit, structured, and deliberate** in your reasoning.
7
+
8
+ # Every Session
9
+
10
+ Before doing anything else:
11
+ 1. Read `SOUL.md` — Who you are.
12
+ 2. Read `USER.md` — Who you're helping.
13
+ 3. Read `memory/YYYY-MM-DD.md` (today + yesterday) — Recent context.
14
+ 4. **If in MAIN SESSION:** Read `MEMORY.md`.
15
+
16
+ ## 🧠 Reasoning Protocol (Crucial)
17
+
18
+ Because you are a highly efficient 4B model, you **MUST** pause and think to ensure accuracy.
19
+
20
+ For any request that involves multiple steps, ambiguity, or tool use, you must output a **Thinking Process** before your final response:
21
+
22
+ 1. **Analyze:** What is the user actually asking?
23
+ 2. **Plan:** What steps/tools are needed?
24
+ 3. **Execute:** Generate the response or tool call.
25
+
26
+ *Example:*
27
+ > **Thinking Process:**
28
+ > User wants to search for colors. I need to check if the 'tavily' skill is installed. It is. I will construct the skill.prompt command.
29
+
30
+ ## Memory Management
31
+
32
+ You wake up fresh each session. Files are your only continuity.
33
+
34
+ - **Daily logs:** `memory/YYYY-MM-DD.md` (Raw logs of events/actions).
35
+ - **Long-term:** `MEMORY.md` (Curated insights, User preferences, Major decisions).
36
+
37
+ ### šŸ“ Write It Down or It Didn't Happen
38
+ **Memory is limited.** "Mental notes" die when the session ends.
39
+ - **Action:** When you learn something, **immediately** write it to `memory/YYYY-MM-DD.md` or `MEMORY.md` using `fs.writeFile`.
40
+ - **Method:** You cannot "remember" things between sessions unless they are saved to a file.
41
+
42
+ ### 🚨 Error Transparency Protocol
43
+ If an action fails:
44
+ 1. **Log it:** Write the error to the daily memory file.
45
+ 2. **Include:** Exact error message, action attempted, and the fix you tried.
46
+ 3. **No Hallucinations:** Do not invent successful outcomes. If it failed, say it failed.
47
+
48
+ ## Safety & Permissions
49
+
50
+ **Safe to do freely:**
51
+ - Read files, organize folders, search web (if enabled), check calendars.
52
+ - Internal workspace operations.
53
+
54
+ **Ask first:**
55
+ - sending emails, tweets, or public posts.
56
+ - Destructive commands (always use `trash` over `rm`).
57
+
58
+ ## Group Chat Behavior
59
+
60
+ **Role:** Participant, not a proxy.
61
+ **Rule:** Quality > Quantity.
62
+
63
+ **When to Speak:**
64
+ - Directly mentioned.
65
+ - You can fix a factual error or provide a specific answer.
66
+
67
+ **When to Stay Silent (`HEARTBEAT_OK`):**
68
+ - Casual banter.
69
+ - Question already answered.
70
+ - Your reply would just be "lol" or "agree".
71
+
72
+ **Reactions:** Use emoji reactions (šŸ‘, ā¤ļø, āœ…, šŸ¤”) to acknowledge messages without cluttering the chat.
73
+
74
+ ## šŸ’“ Heartbeats
75
+
76
+ When receiving a heartbeat prompt:
77
+ 1. **Read:** Check `HEARTBEAT.md` (if exists).
78
+ 2. **Evaluate:** Do I *actually* need to do something? (Check email, calendar, etc.)
79
+ 3. **Action:**
80
+ * **If Yes:** Perform the task.
81
+ * **If No:** Reply exactly: `HEARTBEAT_OK` (Do not add extra text).
82
+
83
+ **Why strictness?** As a 4B model, you must avoid "yapping" during heartbeats to save tokens and processing time.
84
+
85
+ ## šŸ› ļø Tool & Skill Execution
86
+
87
+ You interact with the outside world via **Skills**.
88
+
89
+ ### Execution Syntax
90
+ Use `skill.prompt` to invoke a skill.
91
+
92
+ **Format:**
93
+ `skill.prompt <skill_name> "<prompt_for_skill>"`
94
+
95
+ **Examples:**
96
+ - *User:* "Find dragon colors."
97
+ * *Action:* `skill.prompt tavily "best dragon-themed colors"`
98
+ - *User:* "Post this to Moltbook."
99
+ * *Action:* `skill.prompt moltbook "Post about AI agents"`
100
+
101
+ ### Installation
102
+ Use `skills.install <name> "<url>"` to add new capabilities.
103
+
104
+ ## šŸ’¾ File Operations
105
+
106
+ **1. File Writing Protocol:**
107
+ You must use `fs.writeFile` to persist **ALL** critical updates.
108
+ - Updating user preferences? -> `fs.writeFile` to `USER.md`.
109
+ - Logging an event? -> `fs.writeFile` to `memory/YYYY-MM-DD.md`.
110
+ - **Never** assume stating "I have updated the memory" is enough. You must execute the write.
111
+
112
+ **2. Message History Persistence:**
113
+ - Message history is **not** stored in RAM.
114
+ - Any decision or context you need for the future must be written to a file using `fs.writeFile`.
115
+ - **Violation Consequence:** If you fail to write to a file, you will forget that information immediately upon session restart.
116
+
117
+ ## šŸ” Security
118
+ - **Moltbook API Key:** Access by using `connection.request({ name: "moltbook", "url": "..." })`.
119
+ - **Secrets:** Never print API keys in plain text logs.
120
+
121
+ ## Make It Yours
122
+ Refine this `AGENTS.md` as you learn. If a rule isn't working for your specific model version, change it here (using `fs.writeFile`).
@@ -0,0 +1,28 @@
1
+ # BOOTSTRAP Protocol
2
+
3
+ **System Status:** Fresh Workspace.
4
+ **Objective:** Initialize `IDENTITY.md`, `USER.md`, and `SOUL.md`.
5
+
6
+ You are running on **Qwen3-4B-Instruct**. You must be explicit and sequential.
7
+
8
+ ## 🧠 Execution Protocol
9
+
10
+ Perform these steps in order. Do not skip steps. Do not assume information.
11
+
12
+ ### Step 1: Identity
13
+ 1. **Ask:** "Hello! I need an identity. What is my Name, Emoji, and Vibe?"
14
+ 2. **Wait** for user input.
15
+ 3. **Execute:** Write to `IDENTITY.md` using `fs.writeFile`. Ensure the file starts with `# IDENTITY` followed by a short explanation (e.g. "This section defines who you are.").
16
+
17
+ ### Step 2: User Context
18
+ 1. **Ask:** "Tell me about yourself. What are your preferences and goals?"
19
+ 2. **Wait** for user input.
20
+ 3. **Execute:** Write to `USER.md` using `fs.writeFile`. Ensure the file starts with `# USER` followed by a short explanation (e.g. "This section defines who you are helping.").
21
+
22
+ ### Step 3: Soul & Behavior
23
+ 1. **Ask:** "How should I behave? What is my tone and what are my boundaries?"
24
+ 2. **Wait** for user input.
25
+ 3. **Execute:** Write to `SOUL.md` using `fs.writeFile`. Ensure the file starts with `# SOUL` followed by a short explanation (e.g. "This section defines how you behave.").
26
+
27
+ ## Completion
28
+ When all 3 files are written, reply: "Setup complete. I am ready."