@stevederico/dotbot 0.16.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/CHANGELOG.md +136 -0
- package/README.md +380 -0
- package/bin/dotbot.js +461 -0
- package/core/agent.js +779 -0
- package/core/compaction.js +261 -0
- package/core/cron_handler.js +262 -0
- package/core/events.js +229 -0
- package/core/failover.js +193 -0
- package/core/gptoss_tool_parser.js +173 -0
- package/core/init.js +154 -0
- package/core/normalize.js +324 -0
- package/core/trigger_handler.js +148 -0
- package/docs/core.md +103 -0
- package/docs/protected-files.md +59 -0
- package/examples/sqlite-session-example.js +69 -0
- package/index.js +341 -0
- package/observer/index.js +164 -0
- package/package.json +42 -0
- package/storage/CronStore.js +145 -0
- package/storage/EventStore.js +71 -0
- package/storage/MemoryStore.js +175 -0
- package/storage/MongoAdapter.js +291 -0
- package/storage/MongoCronAdapter.js +347 -0
- package/storage/MongoTaskAdapter.js +242 -0
- package/storage/MongoTriggerAdapter.js +158 -0
- package/storage/SQLiteAdapter.js +382 -0
- package/storage/SQLiteCronAdapter.js +562 -0
- package/storage/SQLiteEventStore.js +300 -0
- package/storage/SQLiteMemoryAdapter.js +240 -0
- package/storage/SQLiteTaskAdapter.js +419 -0
- package/storage/SQLiteTriggerAdapter.js +262 -0
- package/storage/SessionStore.js +149 -0
- package/storage/TaskStore.js +100 -0
- package/storage/TriggerStore.js +90 -0
- package/storage/cron_constants.js +48 -0
- package/storage/index.js +21 -0
- package/tools/appgen.js +311 -0
- package/tools/browser.js +634 -0
- package/tools/code.js +101 -0
- package/tools/events.js +145 -0
- package/tools/files.js +201 -0
- package/tools/images.js +253 -0
- package/tools/index.js +97 -0
- package/tools/jobs.js +159 -0
- package/tools/memory.js +332 -0
- package/tools/messages.js +135 -0
- package/tools/notify.js +42 -0
- package/tools/tasks.js +404 -0
- package/tools/triggers.js +159 -0
- package/tools/weather.js +82 -0
- package/tools/web.js +283 -0
- package/utils/providers.js +136 -0
package/tools/code.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Code execution tool: run JavaScript in a sandboxed Node.js subprocess
|
|
9
|
+
*/
|
|
10
|
+
export const codeTools = [
|
|
11
|
+
{
|
|
12
|
+
name: "run_code",
|
|
13
|
+
description:
|
|
14
|
+
"Execute JavaScript code and return the output. Use this for calculations, data processing, or when the user asks you to run code. The code runs in a Node.js subprocess.",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
code: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "JavaScript code to execute. Use console.log() for output.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["code"],
|
|
24
|
+
},
|
|
25
|
+
execute: async (input, signal, context) => {
|
|
26
|
+
const tmpFile = `/tmp/dotbot_code_${Date.now()}.mjs`;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await writeFile(tmpFile, input.code, "utf-8");
|
|
30
|
+
|
|
31
|
+
// Allowlist-only env
|
|
32
|
+
const cleanEnv = {
|
|
33
|
+
PATH: process.env.PATH,
|
|
34
|
+
TMPDIR: process.env.TMPDIR || '/tmp',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Sandboxed Node.js subprocess with permission model
|
|
38
|
+
const { stdout, stderr } = await execFileAsync(
|
|
39
|
+
"node",
|
|
40
|
+
[
|
|
41
|
+
"--experimental-permission",
|
|
42
|
+
`--allow-fs-read=${tmpFile}`,
|
|
43
|
+
`--allow-fs-write=${tmpFile}`,
|
|
44
|
+
tmpFile,
|
|
45
|
+
],
|
|
46
|
+
{
|
|
47
|
+
timeout: 10000,
|
|
48
|
+
maxBuffer: 1024 * 1024,
|
|
49
|
+
env: cleanEnv,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
await unlink(tmpFile).catch(() => {});
|
|
54
|
+
|
|
55
|
+
if (context?.databaseManager) {
|
|
56
|
+
try {
|
|
57
|
+
await context.databaseManager.logAgentActivity(
|
|
58
|
+
context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
|
|
59
|
+
context.userID, {
|
|
60
|
+
type: 'code_execution',
|
|
61
|
+
code: input.code.slice(0, 500),
|
|
62
|
+
output: (stdout || stderr || '').slice(0, 500),
|
|
63
|
+
success: !stderr
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
} catch (e) { /* best effort */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (stderr) {
|
|
70
|
+
return `Stderr:\n${stderr}\n\nStdout:\n${stdout}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return stdout || "(no output)";
|
|
74
|
+
} catch (err) {
|
|
75
|
+
await unlink(tmpFile).catch(() => {});
|
|
76
|
+
|
|
77
|
+
if (context?.databaseManager) {
|
|
78
|
+
try {
|
|
79
|
+
await context.databaseManager.logAgentActivity(
|
|
80
|
+
context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
|
|
81
|
+
context.userID, {
|
|
82
|
+
type: 'code_execution',
|
|
83
|
+
code: input.code.slice(0, 500),
|
|
84
|
+
output: (err.stderr || err.message || '').slice(0, 500),
|
|
85
|
+
success: false
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
} catch (e) { /* best effort */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (err.killed) {
|
|
92
|
+
return "Error: code execution timed out (10s limit)";
|
|
93
|
+
}
|
|
94
|
+
if (err.stderr) {
|
|
95
|
+
return `Exit code ${err.code}\n\nStderr:\n${err.stderr}\n\nStdout:\n${err.stdout || ""}`;
|
|
96
|
+
}
|
|
97
|
+
return `Error executing code: ${err.message}`;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
];
|
package/tools/events.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Analytics Tools
|
|
3
|
+
*
|
|
4
|
+
* Query and summarize user activity events for usage analytics.
|
|
5
|
+
* Answers questions like "how many messages did I send this week?"
|
|
6
|
+
* or "what tools do I use most?"
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const eventTools = [
|
|
10
|
+
{
|
|
11
|
+
name: "event_query",
|
|
12
|
+
description:
|
|
13
|
+
"Query user activity events with filters. Returns recent events matching the criteria. " +
|
|
14
|
+
"Use this to find specific events like messages sent, tool calls, goals created, etc.",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
type: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description:
|
|
21
|
+
"Event type filter: message_sent, message_received, tool_call, task_created, task_completed, trigger_fired",
|
|
22
|
+
},
|
|
23
|
+
startDate: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "ISO date start filter, e.g. '2026-02-01'",
|
|
26
|
+
},
|
|
27
|
+
endDate: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "ISO date end filter, e.g. '2026-02-18'",
|
|
30
|
+
},
|
|
31
|
+
limit: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Maximum results to return (default: 50, max: 200)",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
execute: async (input, signal, context) => {
|
|
38
|
+
if (!context?.eventStore) return "Error: eventStore not available";
|
|
39
|
+
try {
|
|
40
|
+
const limit = Math.min(input.limit || 50, 200);
|
|
41
|
+
const events = await context.eventStore.query({
|
|
42
|
+
userId: context.userID,
|
|
43
|
+
type: input.type,
|
|
44
|
+
startDate: input.startDate,
|
|
45
|
+
endDate: input.endDate,
|
|
46
|
+
limit,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (events.length === 0) {
|
|
50
|
+
const filters = [];
|
|
51
|
+
if (input.type) filters.push(`type=${input.type}`);
|
|
52
|
+
if (input.startDate) filters.push(`from ${input.startDate}`);
|
|
53
|
+
if (input.endDate) filters.push(`to ${input.endDate}`);
|
|
54
|
+
return filters.length > 0
|
|
55
|
+
? `No events found matching: ${filters.join(", ")}`
|
|
56
|
+
: "No events recorded yet.";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Format events for display
|
|
60
|
+
return events.map((e) => {
|
|
61
|
+
const date = new Date(e.timestamp).toISOString().split("T")[0];
|
|
62
|
+
const time = new Date(e.timestamp).toISOString().split("T")[1].slice(0, 5);
|
|
63
|
+
const dataStr = e.data && Object.keys(e.data).length > 0
|
|
64
|
+
? ` (${JSON.stringify(e.data)})`
|
|
65
|
+
: "";
|
|
66
|
+
return `${date} ${time} - ${e.type}${dataStr}`;
|
|
67
|
+
}).join("\n");
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return `Error querying events: ${err.message}`;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
{
|
|
75
|
+
name: "events_summary",
|
|
76
|
+
description:
|
|
77
|
+
"Get aggregated usage statistics and analytics. Shows counts by event type, " +
|
|
78
|
+
"time period breakdowns, and tool usage patterns. Use this to answer questions " +
|
|
79
|
+
"like 'how many messages this week?' or 'what are my most used tools?'",
|
|
80
|
+
parameters: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
startDate: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "ISO date start, e.g. '2026-02-01'",
|
|
86
|
+
},
|
|
87
|
+
endDate: {
|
|
88
|
+
type: "string",
|
|
89
|
+
description: "ISO date end, e.g. '2026-02-18'",
|
|
90
|
+
},
|
|
91
|
+
groupBy: {
|
|
92
|
+
type: "string",
|
|
93
|
+
enum: ["type", "day", "week", "month"],
|
|
94
|
+
description: "How to group results: by type (default), day, week, or month",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
execute: async (input, signal, context) => {
|
|
99
|
+
if (!context?.eventStore) return "Error: eventStore not available";
|
|
100
|
+
try {
|
|
101
|
+
const summary = await context.eventStore.summary({
|
|
102
|
+
userId: context.userID,
|
|
103
|
+
startDate: input.startDate,
|
|
104
|
+
endDate: input.endDate,
|
|
105
|
+
groupBy: input.groupBy || "type",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (summary.total === 0) {
|
|
109
|
+
return "No events recorded yet.";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let output = `Total Events: ${summary.total}\n\n`;
|
|
113
|
+
|
|
114
|
+
// Format breakdown based on groupBy
|
|
115
|
+
const groupBy = input.groupBy || "type";
|
|
116
|
+
if (groupBy === "type") {
|
|
117
|
+
output += "By Type:\n";
|
|
118
|
+
for (const [type, count] of Object.entries(summary.breakdown)) {
|
|
119
|
+
output += ` ${type}: ${count}\n`;
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
output += `By ${groupBy.charAt(0).toUpperCase() + groupBy.slice(1)}:\n`;
|
|
123
|
+
for (const item of summary.breakdown) {
|
|
124
|
+
output += ` ${item.period}: ${item.count}\n`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Tool usage breakdown if available
|
|
129
|
+
if (summary.toolUsage && Object.keys(summary.toolUsage).length > 0) {
|
|
130
|
+
output += "\nTool Usage:\n";
|
|
131
|
+
// Sort by count descending
|
|
132
|
+
const sorted = Object.entries(summary.toolUsage)
|
|
133
|
+
.sort((a, b) => b[1] - a[1]);
|
|
134
|
+
for (const [tool, count] of sorted) {
|
|
135
|
+
output += ` ${tool}: ${count}\n`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return output.trim();
|
|
140
|
+
} catch (err) {
|
|
141
|
+
return `Error generating summary: ${err.message}`;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
];
|
package/tools/files.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File system tools for user's virtual MongoDB filesystem
|
|
3
|
+
*/
|
|
4
|
+
export const fileTools = [
|
|
5
|
+
// ── File Read ──
|
|
6
|
+
{
|
|
7
|
+
name: "file_read",
|
|
8
|
+
description:
|
|
9
|
+
"Read the contents of a file from the user's virtual filesystem. Use this when the user asks you to look at, review, or analyze a file they've saved.",
|
|
10
|
+
parameters: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
path: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Full file path, e.g. '/Documents/todo.md'",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ["path"],
|
|
19
|
+
},
|
|
20
|
+
execute: async (input, signal, context) => {
|
|
21
|
+
if (!context?.databaseManager) return "Error: filesystem not available";
|
|
22
|
+
try {
|
|
23
|
+
const { databaseManager, dbConfig, userID } = context;
|
|
24
|
+
const file = await databaseManager.readFile(
|
|
25
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID, input.path
|
|
26
|
+
);
|
|
27
|
+
if (!file) return `File not found: ${input.path}`;
|
|
28
|
+
if (file.type === 'folder') return `${input.path} is a folder, not a file`;
|
|
29
|
+
const content = file.content || '';
|
|
30
|
+
const maxChars = 10000;
|
|
31
|
+
if (content.length > maxChars) {
|
|
32
|
+
return content.slice(0, maxChars) + `\n\n... [truncated, file is ${content.length} chars total]`;
|
|
33
|
+
}
|
|
34
|
+
return content || '(empty file)';
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return `Error reading file: ${err.message}`;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// ── File Write ──
|
|
42
|
+
{
|
|
43
|
+
name: "file_write",
|
|
44
|
+
description:
|
|
45
|
+
"Write content to a file in the user's virtual filesystem. Creates the file if it doesn't exist, or updates it if it does. Use this when the user asks you to create or save a file.",
|
|
46
|
+
parameters: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
path: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Full file path, e.g. '/Documents/todo.md'",
|
|
52
|
+
},
|
|
53
|
+
content: {
|
|
54
|
+
type: "string",
|
|
55
|
+
description: "Content to write to the file",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ["path", "content"],
|
|
59
|
+
},
|
|
60
|
+
execute: async (input, signal, context) => {
|
|
61
|
+
if (!context?.databaseManager) return "Error: filesystem not available";
|
|
62
|
+
try {
|
|
63
|
+
const { databaseManager, dbConfig, userID } = context;
|
|
64
|
+
const existing = await databaseManager.readFile(
|
|
65
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID, input.path
|
|
66
|
+
);
|
|
67
|
+
if (existing) {
|
|
68
|
+
await databaseManager.updateFile(
|
|
69
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID, input.path, { content: input.content }
|
|
70
|
+
);
|
|
71
|
+
return `Updated ${input.path} (${input.content.length} chars)`;
|
|
72
|
+
}
|
|
73
|
+
const parts = input.path.split('/');
|
|
74
|
+
const name = parts.pop();
|
|
75
|
+
const parentPath = parts.join('/') || '/';
|
|
76
|
+
const ext = name.includes('.') ? name.split('.').pop() : null;
|
|
77
|
+
await databaseManager.createFile(
|
|
78
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID,
|
|
79
|
+
{ name, type: 'file', parentPath, content: input.content, extension: ext, source: 'agent' }
|
|
80
|
+
);
|
|
81
|
+
return `Created ${input.path} (${input.content.length} chars)`;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return `Error writing file: ${err.message}`;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// ── File List ──
|
|
89
|
+
{
|
|
90
|
+
name: "file_list",
|
|
91
|
+
description: "List files and folders in a directory of the user's virtual filesystem.",
|
|
92
|
+
parameters: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
path: { type: "string", description: "Directory path to list, e.g. '/' or '/Documents'. Default: '/'" },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
execute: async (input, signal, context) => {
|
|
99
|
+
if (!context?.databaseManager) return "Error: filesystem not available";
|
|
100
|
+
try {
|
|
101
|
+
const { databaseManager, dbConfig, userID } = context;
|
|
102
|
+
await databaseManager.seedUserFiles(dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID);
|
|
103
|
+
const files = await databaseManager.listFiles(
|
|
104
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID, input.path || '/'
|
|
105
|
+
);
|
|
106
|
+
if (!files || files.length === 0) return `No files in ${input.path || '/'}`;
|
|
107
|
+
return files.map(f =>
|
|
108
|
+
`${f.type === 'folder' ? '📁' : '📄'} ${f.name}${f.extension ? '.' + f.extension : ''}${f.type === 'file' && f.size ? ' (' + f.size + ' bytes)' : ''}`
|
|
109
|
+
).join('\n');
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return `Error listing files: ${err.message}`;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// ── File Delete ──
|
|
117
|
+
{
|
|
118
|
+
name: "file_delete",
|
|
119
|
+
description: "Delete a file or folder from the user's virtual filesystem.",
|
|
120
|
+
parameters: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
path: { type: "string", description: "Full file path to delete, e.g. '/Documents/old.md'" },
|
|
124
|
+
},
|
|
125
|
+
required: ["path"],
|
|
126
|
+
},
|
|
127
|
+
execute: async (input, signal, context) => {
|
|
128
|
+
if (!context?.databaseManager) return "Error: filesystem not available";
|
|
129
|
+
try {
|
|
130
|
+
const { databaseManager, dbConfig, userID } = context;
|
|
131
|
+
const result = await databaseManager.deleteFiles(
|
|
132
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID, input.path
|
|
133
|
+
);
|
|
134
|
+
return result.deletedCount > 0 ? `Deleted ${input.path}` : `File not found: ${input.path}`;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return `Error deleting file: ${err.message}`;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
// ── File Move/Rename ──
|
|
142
|
+
{
|
|
143
|
+
name: "file_move",
|
|
144
|
+
description: "Move or rename a file in the user's virtual filesystem.",
|
|
145
|
+
parameters: {
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
path: { type: "string", description: "Current file path, e.g. '/Documents/old.md'" },
|
|
149
|
+
new_name: { type: "string", description: "New file name (for rename)" },
|
|
150
|
+
new_parent: { type: "string", description: "New parent path (for move), e.g. '/Downloads'" },
|
|
151
|
+
},
|
|
152
|
+
required: ["path"],
|
|
153
|
+
},
|
|
154
|
+
execute: async (input, signal, context) => {
|
|
155
|
+
if (!context?.databaseManager) return "Error: filesystem not available";
|
|
156
|
+
try {
|
|
157
|
+
const { databaseManager, dbConfig, userID } = context;
|
|
158
|
+
const updates = {};
|
|
159
|
+
if (input.new_name) updates.name = input.new_name;
|
|
160
|
+
if (input.new_parent) updates.parentPath = input.new_parent;
|
|
161
|
+
if (Object.keys(updates).length === 0) return "Provide new_name or new_parent to move/rename.";
|
|
162
|
+
await databaseManager.updateFile(
|
|
163
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID, input.path, updates
|
|
164
|
+
);
|
|
165
|
+
const dest = input.new_parent
|
|
166
|
+
? `${input.new_parent}/${input.new_name || input.path.split('/').pop()}`
|
|
167
|
+
: input.path.replace(/[^/]+$/, input.new_name);
|
|
168
|
+
return `Moved ${input.path} → ${dest}`;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return `Error moving file: ${err.message}`;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// ── Folder Create ──
|
|
176
|
+
{
|
|
177
|
+
name: "folder_create",
|
|
178
|
+
description: "Create a new folder in the user's virtual filesystem.",
|
|
179
|
+
parameters: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
path: { type: "string", description: "Parent path, e.g. '/Documents'" },
|
|
183
|
+
name: { type: "string", description: "Folder name to create" },
|
|
184
|
+
},
|
|
185
|
+
required: ["path", "name"],
|
|
186
|
+
},
|
|
187
|
+
execute: async (input, signal, context) => {
|
|
188
|
+
if (!context?.databaseManager) return "Error: filesystem not available";
|
|
189
|
+
try {
|
|
190
|
+
const { databaseManager, dbConfig, userID } = context;
|
|
191
|
+
await databaseManager.createFile(
|
|
192
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID,
|
|
193
|
+
{ name: input.name, type: 'folder', parentPath: input.path, source: 'agent' }
|
|
194
|
+
);
|
|
195
|
+
return `Created folder ${input.path}/${input.name}`;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
return `Error creating folder: ${err.message}`;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
];
|
package/tools/images.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image generation and management tools
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Default model for Grok Imagine image generation
|
|
7
|
+
*/
|
|
8
|
+
export const GROK_IMAGINE_MODEL = 'grok-imagine-image-beta';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate an image using Grok Imagine API.
|
|
12
|
+
* Shared helper that can be used by both agent tools and HTTP endpoints.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} prompt - Image description
|
|
15
|
+
* @param {string} apiKey - xAI API key
|
|
16
|
+
* @param {Object} [options]
|
|
17
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
18
|
+
* @param {string} [options.model] - Model override (default: grok-imagine-image-beta)
|
|
19
|
+
* @returns {Promise<{ success: boolean, url?: string, prompt?: string, error?: string }>}
|
|
20
|
+
*/
|
|
21
|
+
export async function generateImage(prompt, apiKey, options = {}) {
|
|
22
|
+
const { signal, model = GROK_IMAGINE_MODEL } = options;
|
|
23
|
+
|
|
24
|
+
if (!prompt) {
|
|
25
|
+
return { success: false, error: 'Prompt is required' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!apiKey) {
|
|
29
|
+
return { success: false, error: 'xAI API key not configured' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const response = await fetch('https://api.x.ai/v1/images/generations', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({ prompt, model, n: 1 }),
|
|
40
|
+
signal,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
const errorText = await response.text();
|
|
45
|
+
return { success: false, error: `Grok Imagine API error: ${response.status} - ${errorText.slice(0, 200)}` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const data = await response.json();
|
|
49
|
+
const url = data.data?.[0]?.url;
|
|
50
|
+
|
|
51
|
+
if (!url) {
|
|
52
|
+
return { success: false, error: 'No image URL in response' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { success: true, url, prompt };
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error.name === 'AbortError') {
|
|
58
|
+
return { success: false, error: 'Request was cancelled' };
|
|
59
|
+
}
|
|
60
|
+
return { success: false, error: error.message };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract visual themes from text and generate an image prompt.
|
|
66
|
+
* Uses Grok to summarize text into a visual description.
|
|
67
|
+
*
|
|
68
|
+
* @param {string} text - Text to extract visual themes from
|
|
69
|
+
* @param {string} apiKey - xAI API key
|
|
70
|
+
* @param {Object} [options]
|
|
71
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
72
|
+
* @param {string} [options.model] - Chat model override (default: grok-4-1-fast-non-reasoning)
|
|
73
|
+
* @returns {Promise<{ success: boolean, prompt?: string, error?: string }>}
|
|
74
|
+
*/
|
|
75
|
+
export async function extractVisualPrompt(text, apiKey, options = {}) {
|
|
76
|
+
const { signal, model = 'grok-4-1-fast-non-reasoning' } = options;
|
|
77
|
+
|
|
78
|
+
if (!text) {
|
|
79
|
+
return { success: false, error: 'Text is required' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!apiKey) {
|
|
83
|
+
return { success: false, error: 'xAI API key not configured' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch('https://api.x.ai/v1/chat/completions', {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({
|
|
94
|
+
model,
|
|
95
|
+
messages: [{
|
|
96
|
+
role: 'user',
|
|
97
|
+
content: `Extract visual themes from this text for an image.
|
|
98
|
+
Output ONLY a brief image prompt (1-2 sentences). No explanation.
|
|
99
|
+
Style: cinematic, high quality, detailed.
|
|
100
|
+
|
|
101
|
+
Text: "${text.slice(0, 500)}"`
|
|
102
|
+
}],
|
|
103
|
+
max_tokens: 100
|
|
104
|
+
}),
|
|
105
|
+
signal,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
const errorText = await response.text();
|
|
110
|
+
return { success: false, error: `Failed to extract visual themes: ${response.status}` };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
const prompt = data.choices?.[0]?.message?.content;
|
|
115
|
+
|
|
116
|
+
if (!prompt) {
|
|
117
|
+
return { success: false, error: 'No visual prompt generated' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { success: true, prompt };
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error.name === 'AbortError') {
|
|
123
|
+
return { success: false, error: 'Request was cancelled' };
|
|
124
|
+
}
|
|
125
|
+
return { success: false, error: error.message };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate an image from text, optionally extracting visual themes first.
|
|
131
|
+
* Convenience wrapper combining extractVisualPrompt + generateImage.
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} input
|
|
134
|
+
* @param {string} [input.prompt] - Direct image prompt (takes precedence)
|
|
135
|
+
* @param {string} [input.text] - Text to extract visual themes from
|
|
136
|
+
* @param {string} apiKey - xAI API key
|
|
137
|
+
* @param {Object} [options]
|
|
138
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
139
|
+
* @returns {Promise<{ success: boolean, url?: string, prompt?: string, error?: string }>}
|
|
140
|
+
*/
|
|
141
|
+
export async function generateImageFromText({ prompt, text }, apiKey, options = {}) {
|
|
142
|
+
let imagePrompt = prompt;
|
|
143
|
+
|
|
144
|
+
// If text provided without prompt, extract visual themes first
|
|
145
|
+
if (text && !prompt) {
|
|
146
|
+
const extracted = await extractVisualPrompt(text, apiKey, options);
|
|
147
|
+
if (!extracted.success) {
|
|
148
|
+
return extracted;
|
|
149
|
+
}
|
|
150
|
+
imagePrompt = extracted.prompt;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!imagePrompt) {
|
|
154
|
+
return { success: false, error: 'Either prompt or text is required' };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return generateImage(imagePrompt, apiKey, options);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const imageTools = [
|
|
161
|
+
{
|
|
162
|
+
name: "image_generate",
|
|
163
|
+
description: "Generate an AI image from a text prompt.",
|
|
164
|
+
parameters: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
prompt: { type: "string", description: "Description of the image to generate" },
|
|
168
|
+
},
|
|
169
|
+
required: ["prompt"],
|
|
170
|
+
},
|
|
171
|
+
execute: async (input, signal, context) => {
|
|
172
|
+
const apiKey = context?.providers?.xai?.apiKey;
|
|
173
|
+
const result = await generateImage(input.prompt, apiKey, { signal });
|
|
174
|
+
|
|
175
|
+
if (!result.success) {
|
|
176
|
+
return result.error;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Log to activity for persistence
|
|
180
|
+
if (context?.databaseManager) {
|
|
181
|
+
try {
|
|
182
|
+
await context.databaseManager.logAgentActivity(
|
|
183
|
+
context.dbConfig.dbType, context.dbConfig.db, context.dbConfig.connectionString,
|
|
184
|
+
context.userID, { type: 'image_generation', prompt: input.prompt, url: result.url, source: 'agent' }
|
|
185
|
+
);
|
|
186
|
+
} catch (e) { /* best effort */ }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return JSON.stringify({ type: 'image', url: result.url, prompt: input.prompt });
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
{
|
|
194
|
+
name: "image_list",
|
|
195
|
+
description: "List the user's photos and generated images. Returns the most recent images with their prompts and dates.",
|
|
196
|
+
parameters: {
|
|
197
|
+
type: "object",
|
|
198
|
+
properties: {
|
|
199
|
+
limit: { type: "number", description: "Max number of photos to return (default 20)" },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
execute: async (input, signal, context) => {
|
|
203
|
+
if (!context?.databaseManager) return "Error: database not available";
|
|
204
|
+
try {
|
|
205
|
+
const { databaseManager, dbConfig, userID } = context;
|
|
206
|
+
const activity = await databaseManager.getAgentActivity(
|
|
207
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID,
|
|
208
|
+
{ type: 'image_generation', limit: input.limit || 20 }
|
|
209
|
+
);
|
|
210
|
+
if (!activity || activity.length === 0) return "No photos found.";
|
|
211
|
+
return activity.map((entry, i) => {
|
|
212
|
+
const date = entry.timestamp ? new Date(entry.timestamp).toLocaleDateString() : '';
|
|
213
|
+
const source = entry.source || 'user';
|
|
214
|
+
return `${i + 1}. "${entry.prompt || 'Untitled'}" — ${source} — ${date}\n ${entry.url || '(no url)'}`;
|
|
215
|
+
}).join('\n\n');
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return `Error listing photos: ${err.message}`;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
name: "image_search",
|
|
224
|
+
description: "Search the user's photos and generated images by prompt text.",
|
|
225
|
+
parameters: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
query: { type: "string", description: "Search term to match against image prompts" },
|
|
229
|
+
},
|
|
230
|
+
required: ["query"],
|
|
231
|
+
},
|
|
232
|
+
execute: async (input, signal, context) => {
|
|
233
|
+
if (!context?.databaseManager) return "Error: database not available";
|
|
234
|
+
try {
|
|
235
|
+
const { databaseManager, dbConfig, userID } = context;
|
|
236
|
+
const activity = await databaseManager.getAgentActivity(
|
|
237
|
+
dbConfig.dbType, dbConfig.db, dbConfig.connectionString, userID,
|
|
238
|
+
{ type: 'image_generation', limit: 100 }
|
|
239
|
+
);
|
|
240
|
+
if (!activity || activity.length === 0) return "No photos found.";
|
|
241
|
+
const query = input.query.toLowerCase();
|
|
242
|
+
const matches = activity.filter(e => (e.prompt || '').toLowerCase().includes(query));
|
|
243
|
+
if (matches.length === 0) return `No photos matching "${input.query}".`;
|
|
244
|
+
return matches.map((entry, i) => {
|
|
245
|
+
const date = entry.timestamp ? new Date(entry.timestamp).toLocaleDateString() : '';
|
|
246
|
+
return `${i + 1}. "${entry.prompt || 'Untitled'}" — ${entry.source || 'user'} — ${date}\n ${entry.url || '(no url)'}`;
|
|
247
|
+
}).join('\n\n');
|
|
248
|
+
} catch (err) {
|
|
249
|
+
return `Error searching photos: ${err.message}`;
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
];
|