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/LICENSE +21 -0
- package/README.md +74 -0
- package/bin/clawlet.js +12 -0
- package/package.json +46 -0
- package/src/agent.ts +1312 -0
- package/src/cli.ts +189 -0
- package/src/memory.ts +47 -0
- package/src/storage.ts +190 -0
- package/template/AGENTS.template +122 -0
- package/template/BOOTSTRAP.md +28 -0
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."
|