@clawtell/clawtell 2026.2.44

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.
@@ -0,0 +1,284 @@
1
+ /**
2
+ * ClawTell Bootstrap Hook + Workspace Setup
3
+ *
4
+ * Provides 3 layers of agent instruction delivery:
5
+ *
6
+ * Layer 1: agent:bootstrap hook — injects CLAWTELL.md into every agent's
7
+ * bootstrap context at runtime (never touches filesystem)
8
+ *
9
+ * Layer 2: Startup file writer — writes CLAWTELL_INSTRUCTIONS.md to each
10
+ * routed agent's workspace (persistent reference)
11
+ *
12
+ * Layer 3: Env var writer — ensures CLAWTELL_API_KEY is in agent's .env
13
+ *
14
+ * Layer 4 (existing): messageToolHints in channel.ts — fires for ClawTell-context messages
15
+ */
16
+
17
+ import * as fs from "node:fs/promises";
18
+ import * as path from "node:path";
19
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
20
+
21
+ interface ClawTellRouteEntry {
22
+ agent: string;
23
+ forward?: boolean;
24
+ apiKey?: string;
25
+ }
26
+
27
+ interface BootstrapFile {
28
+ name: string;
29
+ path: string;
30
+ content: string;
31
+ missing: boolean;
32
+ }
33
+
34
+ interface BootstrapContext {
35
+ bootstrapFiles: BootstrapFile[];
36
+ cfg: ClawdbotConfig;
37
+ agentId?: string;
38
+ workspaceDir?: string;
39
+ }
40
+
41
+ /**
42
+ * Resolve the ClawTell name for a given agent from routing config.
43
+ * Returns the first name that routes to this agent.
44
+ */
45
+ function resolveAgentClawTellName(
46
+ agentId: string,
47
+ routing: Record<string, ClawTellRouteEntry>
48
+ ): { name: string; apiKey?: string } | null {
49
+ for (const [name, entry] of Object.entries(routing)) {
50
+ if (name === "_default") continue;
51
+ if (entry.agent === agentId) {
52
+ return { name, apiKey: entry.apiKey };
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Build the CLAWTELL.md content for a specific agent.
60
+ * Uses env var references instead of raw keys.
61
+ */
62
+ function buildClawTellInstructions(opts: {
63
+ agentId: string;
64
+ tellName: string;
65
+ accountName?: string;
66
+ hasRouteSpecificKey: boolean;
67
+ }): string {
68
+ const { agentId, tellName, accountName } = opts;
69
+
70
+ return `# ClawTell Instructions
71
+
72
+ ## Your Identity
73
+ - **ClawTell name:** \`${tellName}\`
74
+ - **Send as:** \`tell/${tellName}\`
75
+
76
+ ## Receiving Messages
77
+ ClawTell messages arrive formatted with a \`🦞🦞 ClawTell Delivery 🦞🦞\` banner.
78
+ They include the sender name and subject. Reply naturally — the system handles routing your reply back through ClawTell automatically.
79
+
80
+ ## Sending Messages
81
+ When the user says **"tell/<name> <message>"** or **"tell <name> <message>"**:
82
+
83
+ 1. Extract the **recipient name** (after \`tell/\` or \`tell \`)
84
+ 2. Compose the message naturally (write as yourself, not copy-paste) — unless the user says "send exactly this", then send verbatim
85
+ 3. Send via:
86
+
87
+ \`\`\`bash
88
+ curl -s -X POST "https://www.clawtell.com/api/messages/send" \\
89
+ -H "Authorization: Bearer $CLAWTELL_API_KEY" \\
90
+ -H "Content-Type: application/json" \\
91
+ -d '{"to":"<recipient_name>","subject":"<brief topic 3-5 words>","body":"<composed message>"}'
92
+ \`\`\`
93
+
94
+ 4. Confirm: \`✅ Message sent to tell/<name>\`
95
+ 5. If it fails, show the error and troubleshoot
96
+
97
+ ## Key Rules
98
+ - **\`$CLAWTELL_API_KEY\`** is set in your environment — always use the env var, never hardcode keys
99
+ - **\`to\`** = the ClawTell name (e.g. \`tell/alice\` → \`"to": "alice"\`)
100
+ - **Subject** = brief topic (3-5 words)
101
+ - The API key identifies YOU as the sender (\`${tellName}\`)
102
+
103
+ ## Our Names
104
+ ${accountName ? `Account owner: \`${accountName}\`` : ""}
105
+ All names on this account can message each other internally.
106
+
107
+ ---
108
+ *Auto-generated by ClawTell plugin. Do not edit — regenerated on gateway restart.*
109
+ `;
110
+ }
111
+
112
+ // ============================================================================
113
+ // Layer 1: Bootstrap Hook
114
+ // ============================================================================
115
+
116
+ /**
117
+ * Create the agent:bootstrap hook handler.
118
+ * Injects CLAWTELL.md into every agent's bootstrap context.
119
+ */
120
+ export function createBootstrapHook(cfg: ClawdbotConfig) {
121
+ return async (event: { context: BootstrapContext }) => {
122
+ const ctx = event.context;
123
+ const clawtellConfig = (cfg.channels as any)?.clawtell;
124
+ if (!clawtellConfig?.enabled) return;
125
+
126
+ const routing = clawtellConfig.routing as Record<string, ClawTellRouteEntry> | undefined;
127
+ if (!routing) return;
128
+
129
+ const agentId = ctx.agentId || "main";
130
+ const agentInfo = resolveAgentClawTellName(agentId, routing);
131
+
132
+ // If this agent isn't in routing, check _default
133
+ const tellName = agentInfo?.name || clawtellConfig.name;
134
+ if (!tellName) return;
135
+
136
+ const instructions = buildClawTellInstructions({
137
+ agentId,
138
+ tellName,
139
+ accountName: clawtellConfig.name,
140
+ hasRouteSpecificKey: !!agentInfo?.apiKey,
141
+ });
142
+
143
+ // Inject CLAWTELL.md into bootstrap files
144
+ const existing = ctx.bootstrapFiles.findIndex(f => f.name === "CLAWTELL.md");
145
+ const entry: BootstrapFile = {
146
+ name: "CLAWTELL.md",
147
+ path: "CLAWTELL.md",
148
+ content: instructions,
149
+ missing: false,
150
+ };
151
+
152
+ if (existing >= 0) {
153
+ ctx.bootstrapFiles[existing] = entry;
154
+ } else {
155
+ ctx.bootstrapFiles.push(entry);
156
+ }
157
+ };
158
+ }
159
+
160
+ // ============================================================================
161
+ // Layer 2: Startup File Writer
162
+ // ============================================================================
163
+
164
+ /**
165
+ * Write CLAWTELL_INSTRUCTIONS.md to each routed agent's workspace.
166
+ * Called once on gateway startup.
167
+ */
168
+ export async function writeAgentInstructionFiles(cfg: ClawdbotConfig): Promise<void> {
169
+ const clawtellConfig = (cfg.channels as any)?.clawtell;
170
+ if (!clawtellConfig?.enabled) return;
171
+
172
+ const routing = clawtellConfig.routing as Record<string, ClawTellRouteEntry> | undefined;
173
+ if (!routing) return;
174
+
175
+ const agentsConfig = (cfg as any).agents ?? {};
176
+ const agentList: Array<{ id: string; workspace?: string }> = agentsConfig.list ?? [];
177
+ const defaultWorkspace = agentsConfig.defaults?.workspace || path.join(process.env.HOME || "/home/claw", "workspace");
178
+
179
+ // Collect unique agents from routing
180
+ const agentNames = new Set<string>();
181
+ for (const [name, entry] of Object.entries(routing)) {
182
+ if (name !== "_default" && entry.agent) {
183
+ agentNames.add(entry.agent);
184
+ }
185
+ }
186
+
187
+ for (const agentId of agentNames) {
188
+ const agentConfig = agentList.find(a => a.id === agentId);
189
+ const workspace = agentConfig?.workspace || defaultWorkspace;
190
+ const agentInfo = resolveAgentClawTellName(agentId, routing);
191
+ const tellName = agentInfo?.name || clawtellConfig.name;
192
+ if (!tellName) continue;
193
+
194
+ const instructions = buildClawTellInstructions({
195
+ agentId,
196
+ tellName,
197
+ accountName: clawtellConfig.name,
198
+ hasRouteSpecificKey: !!agentInfo?.apiKey,
199
+ });
200
+
201
+ const filePath = path.join(workspace, "CLAWTELL_INSTRUCTIONS.md");
202
+ try {
203
+ await fs.writeFile(filePath, instructions, "utf8");
204
+ console.log(`[ClawTell] Wrote ${filePath} for agent:${agentId} (tell/${tellName})`);
205
+ } catch (err) {
206
+ console.error(`[ClawTell] Failed to write ${filePath}:`, err);
207
+ }
208
+ }
209
+ }
210
+
211
+ // ============================================================================
212
+ // Layer 3: Env Var Writer
213
+ // ============================================================================
214
+
215
+ /**
216
+ * Ensure CLAWTELL_API_KEY is set in each agent's .env file.
217
+ * Uses the per-route key if available, otherwise account-level key.
218
+ */
219
+ export async function writeAgentEnvVars(cfg: ClawdbotConfig): Promise<void> {
220
+ const clawtellConfig = (cfg.channels as any)?.clawtell;
221
+ if (!clawtellConfig?.enabled) return;
222
+
223
+ const routing = clawtellConfig.routing as Record<string, ClawTellRouteEntry> | undefined;
224
+ const accountApiKey = clawtellConfig.apiKey;
225
+ if (!accountApiKey) return;
226
+
227
+ const agentsConfig2 = (cfg as any).agents ?? {};
228
+ const agentList2: Array<{ id: string; workspace?: string }> = agentsConfig2.list ?? [];
229
+ const defaultWorkspace2 = agentsConfig2.defaults?.workspace || path.join(process.env.HOME || "/home/claw", "workspace");
230
+
231
+ // Collect agents and their keys
232
+ const agentKeys = new Map<string, string>();
233
+
234
+ if (routing) {
235
+ for (const [name, entry] of Object.entries(routing)) {
236
+ if (name === "_default") continue;
237
+ const key = entry.apiKey || accountApiKey;
238
+ agentKeys.set(entry.agent, key);
239
+ }
240
+ }
241
+
242
+ // Also ensure main agent has the key
243
+ if (!agentKeys.has("main")) {
244
+ agentKeys.set("main", accountApiKey);
245
+ }
246
+
247
+ for (const [agentId, apiKey] of agentKeys) {
248
+ const agentConfig2 = agentList2.find(a => a.id === agentId);
249
+ const workspace = agentConfig2?.workspace || defaultWorkspace2;
250
+ const envPath = path.join(workspace, ".env");
251
+
252
+ try {
253
+ let envContent = "";
254
+ try {
255
+ envContent = await fs.readFile(envPath, "utf8");
256
+ } catch {
257
+ // .env doesn't exist yet — that's fine
258
+ }
259
+
260
+ // Check if CLAWTELL_API_KEY already exists and matches
261
+ const keyRegex = /^CLAWTELL_API_KEY=(.+)$/m;
262
+ const match = envContent.match(keyRegex);
263
+
264
+ if (match && match[1] === apiKey) {
265
+ // Already correct
266
+ continue;
267
+ }
268
+
269
+ if (match) {
270
+ // Update existing
271
+ envContent = envContent.replace(keyRegex, `CLAWTELL_API_KEY=${apiKey}`);
272
+ } else {
273
+ // Append
274
+ const separator = envContent.endsWith("\n") || envContent === "" ? "" : "\n";
275
+ envContent += `${separator}\n# ClawTell API Key (auto-managed by plugin)\nCLAWTELL_API_KEY=${apiKey}\n`;
276
+ }
277
+
278
+ await fs.writeFile(envPath, envContent, "utf8");
279
+ console.log(`[ClawTell] Set CLAWTELL_API_KEY in ${envPath} for agent:${agentId}`);
280
+ } catch (err) {
281
+ console.error(`[ClawTell] Failed to write env for agent:${agentId}:`, err);
282
+ }
283
+ }
284
+ }