loom-agent 1.2.33 → 1.2.37
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 +118 -0
- package/README.md +14 -30
- package/bin/loom-bun.js +95 -67
- package/bin/loom-tui.js +21 -1
- package/bin/loom.js +0 -10
- package/package.json +14 -9
- package/scripts/postinstall.js +57 -5
- package/src/connectors/convex.js +356 -0
- package/src/connectors/deploy.js +390 -0
- package/src/connectors/index.js +23 -0
- package/src/connectors/supabase.js +279 -0
- package/src/core/agents.js +32 -2
- package/src/core/cli.js +56 -7
- package/src/core/permissions.js +12 -1
- package/src/core/plugin-cmd.js +1 -0
- package/src/core/session.js +87 -6
- package/src/mcp/mcp-client.js +42 -2
- package/src/skills/skills-manager.js +25 -2
- package/src/tools/browser.js +204 -0
- package/src/tools/index.js +116 -5
- package/src/tui/App.tsx +19 -4
- package/src/tui/components/BreadcrumbBar.tsx +10 -9
- package/src/tui/components/ChatArea.tsx +3 -3
- package/src/tui/components/InputBar.tsx +22 -11
- package/src/tui/components/MdText.tsx +2 -1
- package/src/tui/components/Modals.tsx +31 -17
- package/src/tui/components/PermissionPopup.tsx +15 -12
- package/src/tui/components/Sidebar.tsx +6 -2
- package/src/tui/components/SplashScreen.tsx +17 -24
- package/src/tui/components/SubagentPanel.tsx +1 -1
- package/src/tui/components/ToastOverlay.tsx +23 -12
- package/src/tui/store.ts +4 -2
- package/src/tui/theme.ts +45 -30
- package/src/tui/tool-display.ts +13 -2
- package/docs/web.md +0 -205
- package/src/web/attach.js +0 -242
- package/src/web/graph-view.html +0 -262
- package/src/web/index.html +0 -824
- package/src/web/web-server.js +0 -470
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// Convex connector — real-time backend, database, functions, auth
|
|
2
|
+
const { ConvexHttpClient } = require('convex/browser');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = process.env.LOOM_CONFIG_DIR || path.join(require('os').homedir(), '.loom');
|
|
8
|
+
const CONNECTORS_FILE = path.join(CONFIG_DIR, 'connectors.json');
|
|
9
|
+
|
|
10
|
+
function loadConnectors() {
|
|
11
|
+
try {
|
|
12
|
+
if (fs.existsSync(CONNECTORS_FILE)) {
|
|
13
|
+
return JSON.parse(fs.readFileSync(CONNECTORS_FILE, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
} catch {}
|
|
16
|
+
return { supabase: {}, convex: {}, deploy: {} };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function saveConnectors(cfg) {
|
|
20
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
fs.writeFileSync(CONNECTORS_FILE, JSON.stringify(cfg, null, 2));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getConvexConfig() {
|
|
25
|
+
const cfg = loadConnectors();
|
|
26
|
+
return cfg.convex || {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function setConvexConfig(config) {
|
|
30
|
+
const cfg = loadConnectors();
|
|
31
|
+
cfg.convex = { ...cfg.convex, ...config };
|
|
32
|
+
saveConnectors(cfg);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createConvexClient(overrideConfig = {}) {
|
|
36
|
+
const config = { ...getConvexConfig(), ...overrideConfig };
|
|
37
|
+
if (!config.url) {
|
|
38
|
+
throw new Error('Convex not configured. Run: loom connectors convex --url <deployment-url>');
|
|
39
|
+
}
|
|
40
|
+
return new ConvexHttpClient(config.url);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Query helpers
|
|
44
|
+
async function query(functionName, args = {}) {
|
|
45
|
+
const client = createConvexClient();
|
|
46
|
+
return await client.query(functionName, args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function mutation(functionName, args = {}) {
|
|
50
|
+
const client = createConvexClient();
|
|
51
|
+
return await client.mutation(functionName, args);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function action(functionName, args = {}) {
|
|
55
|
+
const client = createConvexClient();
|
|
56
|
+
return await client.action(functionName, args);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Subscription helpers (for real-time updates)
|
|
60
|
+
function subscribe(functionName, args, onUpdate) {
|
|
61
|
+
const client = createConvexClient();
|
|
62
|
+
return client.onUpdate(functionName, args, onUpdate);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Auth helpers (using Convex's built-in auth)
|
|
66
|
+
async function signIn(email, password) {
|
|
67
|
+
return await mutation('auth:signIn', { email, password });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function signUp(email, password, name) {
|
|
71
|
+
return await mutation('auth:signUp', { email, password, name });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function signOut() {
|
|
75
|
+
return await mutation('auth:signOut', {});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function getCurrentUser() {
|
|
79
|
+
return await query('auth:getCurrentUser', {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function resetPassword(email) {
|
|
83
|
+
return await mutation('auth:resetPassword', { email });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Database operations (using Convex's query/mutation pattern)
|
|
87
|
+
async function list(table, options = {}) {
|
|
88
|
+
const { filter, orderBy, limit, pagination } = options;
|
|
89
|
+
return await query(`${table}:list`, { filter, orderBy, limit, pagination });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function get(table, id) {
|
|
93
|
+
return await query(`${table}:get`, { id });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function create(table, data) {
|
|
97
|
+
return await mutation(`${table}:create`, { data });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function update(table, id, data) {
|
|
101
|
+
return await mutation(`${table}:update`, { id, data });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function remove(table, id) {
|
|
105
|
+
return await mutation(`${table}:delete`, { id });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// File storage (Convex file storage)
|
|
109
|
+
async function uploadFile(file, metadata = {}) {
|
|
110
|
+
// First get upload URL
|
|
111
|
+
const uploadUrl = await action('storage:getUploadUrl', { metadata });
|
|
112
|
+
// Upload to the signed URL
|
|
113
|
+
const response = await fetch(uploadUrl.uploadUrl, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
body: file,
|
|
116
|
+
headers: { 'Content-Type': file.type || 'application/octet-stream' }
|
|
117
|
+
});
|
|
118
|
+
if (!response.ok) throw new Error('Upload failed');
|
|
119
|
+
const storageId = await response.text();
|
|
120
|
+
return { storageId, ...metadata };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function getFileUrl(storageId) {
|
|
124
|
+
return await query('storage:getUrl', { storageId });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Realtime subscriptions
|
|
128
|
+
function watchQuery(functionName, args, callback) {
|
|
129
|
+
const client = createConvexClient();
|
|
130
|
+
return client.watchQuery(functionName, args, callback);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Project scaffolding for Convex
|
|
134
|
+
async function scaffoldConvexProject(projectName, template = 'react') {
|
|
135
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
136
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
137
|
+
|
|
138
|
+
// Create convex directory structure
|
|
139
|
+
const convexDir = path.join(projectPath, 'convex');
|
|
140
|
+
fs.mkdirSync(convexDir, { recursive: true });
|
|
141
|
+
|
|
142
|
+
const templates = {
|
|
143
|
+
'schema.ts': `import { defineSchema, defineTable } from "convex/server";
|
|
144
|
+
import { v } from "convex/values";
|
|
145
|
+
|
|
146
|
+
export default defineSchema({
|
|
147
|
+
users: defineTable({
|
|
148
|
+
name: v.string(),
|
|
149
|
+
email: v.string(),
|
|
150
|
+
avatar: v.optional(v.string()),
|
|
151
|
+
}).index("by_email", ["email"]),
|
|
152
|
+
|
|
153
|
+
messages: defineTable({
|
|
154
|
+
userId: v.id("users"),
|
|
155
|
+
content: v.string(),
|
|
156
|
+
channel: v.string(),
|
|
157
|
+
}).index("by_channel", ["channel"]),
|
|
158
|
+
|
|
159
|
+
tasks: defineTable({
|
|
160
|
+
title: v.string(),
|
|
161
|
+
completed: v.boolean(),
|
|
162
|
+
assigneeId: v.optional(v.id("users")),
|
|
163
|
+
}).index("by_assignee", ["assigneeId"]),
|
|
164
|
+
});`,
|
|
165
|
+
|
|
166
|
+
'auth.ts': `import { mutation, query } from "./_generated/server";
|
|
167
|
+
import { v } from "convex/values";
|
|
168
|
+
|
|
169
|
+
export const signUp = mutation({
|
|
170
|
+
args: { email: v.string(), password: v.string(), name: v.string() },
|
|
171
|
+
handler: async (ctx, args) => {
|
|
172
|
+
const existing = await ctx.db.query("users").filter(q => q.eq(q.field("email"), args.email)).unique();
|
|
173
|
+
if (existing) throw new Error("Email already registered");
|
|
174
|
+
const userId = await ctx.db.insert("users", { name: args.name, email: args.email });
|
|
175
|
+
return { userId };
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export const signIn = mutation({
|
|
180
|
+
args: { email: v.string(), password: v.string() },
|
|
181
|
+
handler: async (ctx, args) => {
|
|
182
|
+
const user = await ctx.db.query("users").filter(q => q.eq(q.field("email"), args.email)).unique();
|
|
183
|
+
if (!user) throw new Error("Invalid credentials");
|
|
184
|
+
// In production, verify password hash here
|
|
185
|
+
return { userId: user._id };
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export const getCurrentUser = query({
|
|
190
|
+
args: {},
|
|
191
|
+
handler: async (ctx) => {
|
|
192
|
+
// Implement session logic
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
});`,
|
|
196
|
+
|
|
197
|
+
'tasks.ts': `import { mutation, query } from "./_generated/server";
|
|
198
|
+
import { v } from "convex/values";
|
|
199
|
+
|
|
200
|
+
export const list = query({
|
|
201
|
+
args: { userId: v.optional(v.id("users")) },
|
|
202
|
+
handler: async (ctx, args) => {
|
|
203
|
+
let q = ctx.db.query("tasks");
|
|
204
|
+
if (args.userId) q = q.filter(q => q.eq(q.field("assigneeId"), args.userId));
|
|
205
|
+
return await q.collect();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
export const create = mutation({
|
|
210
|
+
args: { title: v.string(), assigneeId: v.optional(v.id("users")) },
|
|
211
|
+
handler: async (ctx, args) => {
|
|
212
|
+
const taskId = await ctx.db.insert("tasks", {
|
|
213
|
+
title: args.title,
|
|
214
|
+
completed: false,
|
|
215
|
+
assigneeId: args.assigneeId
|
|
216
|
+
});
|
|
217
|
+
return { taskId };
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export const toggle = mutation({
|
|
222
|
+
args: { id: v.id("tasks") },
|
|
223
|
+
handler: async (ctx, args) => {
|
|
224
|
+
const task = await ctx.db.get(args.id);
|
|
225
|
+
if (!task) throw new Error("Task not found");
|
|
226
|
+
await ctx.db.patch(args.id, { completed: !task.completed });
|
|
227
|
+
return { success: true };
|
|
228
|
+
}
|
|
229
|
+
});`,
|
|
230
|
+
|
|
231
|
+
'chat.ts': `import { mutation, query } from "./_generated/server";
|
|
232
|
+
import { v } from "convex/values";
|
|
233
|
+
|
|
234
|
+
export const sendMessage = mutation({
|
|
235
|
+
args: { channel: v.string(), content: v.string() },
|
|
236
|
+
handler: async (ctx, args) => {
|
|
237
|
+
const user = await ctx.db.query("users").filter(q => q.eq(q.field("email"), "demo@loom")).unique();
|
|
238
|
+
const messageId = await ctx.db.insert("messages", {
|
|
239
|
+
userId: user?._id,
|
|
240
|
+
content: args.content,
|
|
241
|
+
channel: args.channel
|
|
242
|
+
});
|
|
243
|
+
return { messageId };
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
export const getMessages = query({
|
|
248
|
+
args: { channel: v.string(), limit: v.optional(v.number()) },
|
|
249
|
+
handler: async (ctx, args) => {
|
|
250
|
+
let q = ctx.db.query("messages").filter(q => q.eq(q.field("channel"), args.channel));
|
|
251
|
+
if (args.limit) q = q.take(args.limit);
|
|
252
|
+
return await q.collect();
|
|
253
|
+
}
|
|
254
|
+
});`
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Write template files
|
|
258
|
+
for (const [file, content] of Object.entries(templates)) {
|
|
259
|
+
fs.writeFileSync(path.join(convexDir, file), content);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Create package.json with convex
|
|
263
|
+
const packageJson = {
|
|
264
|
+
name: projectName,
|
|
265
|
+
version: '0.0.1',
|
|
266
|
+
private: true,
|
|
267
|
+
scripts: {
|
|
268
|
+
dev: 'convex dev & vite',
|
|
269
|
+
build: 'convex deploy && vite build',
|
|
270
|
+
convex: 'convex'
|
|
271
|
+
},
|
|
272
|
+
dependencies: {
|
|
273
|
+
convex: '^1.14.0',
|
|
274
|
+
react: '^18.3.0',
|
|
275
|
+
'react-dom': '^18.3.0'
|
|
276
|
+
},
|
|
277
|
+
devDependencies: {
|
|
278
|
+
vite: '^5.0.0',
|
|
279
|
+
'@vitejs/plugin-react': '^4.0.0',
|
|
280
|
+
typescript: '^5.0.0'
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
fs.writeFileSync(path.join(projectPath, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
284
|
+
|
|
285
|
+
// Vite config
|
|
286
|
+
fs.writeFileSync(path.join(projectPath, 'vite.config.ts'),
|
|
287
|
+
`import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react'\nexport default defineConfig({ plugins: [react()], server: { port: 3000, host: true } })`);
|
|
288
|
+
|
|
289
|
+
// Basic React app
|
|
290
|
+
fs.mkdirSync(path.join(projectPath, 'src'), { recursive: true });
|
|
291
|
+
fs.writeFileSync(path.join(projectPath, 'src/main.tsx'),
|
|
292
|
+
`import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App'\nimport './index.css'\nReactDOM.createRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>)`);
|
|
293
|
+
|
|
294
|
+
fs.writeFileSync(path.join(projectPath, 'src/App.tsx'),
|
|
295
|
+
`import { useQuery } from 'convex/react'\nimport { api } from '../convex/_generated/api'\n\nfunction App() {\n const tasks = useQuery(api.tasks.list)\n return (\n <div style={{padding: '2rem', fontFamily: 'system-ui'}}\n <h1>${projectName}</h1>\n <ul>\n {tasks?.map(t => <li key={t._id}><input type="checkbox" checked={t.completed} /> {t.title}</li>)}\n </ul>\n </div>\n )\n}\nexport default App`);
|
|
296
|
+
|
|
297
|
+
fs.writeFileSync(path.join(projectPath, 'src/index.css'),
|
|
298
|
+
`* { box-sizing: border-box; } body { margin: 0; font-family: system-ui; background: #fafafa; }`);
|
|
299
|
+
|
|
300
|
+
fs.writeFileSync(path.join(projectPath, 'index.html'),
|
|
301
|
+
`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${projectName}</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>`);
|
|
302
|
+
|
|
303
|
+
// tsconfig
|
|
304
|
+
fs.writeFileSync(path.join(projectPath, 'tsconfig.json'),
|
|
305
|
+
JSON.stringify({
|
|
306
|
+
compilerOptions: {
|
|
307
|
+
target: 'ES2020',
|
|
308
|
+
useDefineForClassFields: true,
|
|
309
|
+
lib: ['ES2020', 'DOM', 'DOM.Iterable'],
|
|
310
|
+
module: 'ESNext',
|
|
311
|
+
skipLibCheck: true,
|
|
312
|
+
moduleResolution: 'bundler',
|
|
313
|
+
allowImportingTsExtensions: true,
|
|
314
|
+
resolveJsonModule: true,
|
|
315
|
+
isolatedModules: true,
|
|
316
|
+
noEmit: true,
|
|
317
|
+
jsx: 'react-jsx',
|
|
318
|
+
strict: true,
|
|
319
|
+
noUnusedLocals: true,
|
|
320
|
+
noUnusedParameters: true,
|
|
321
|
+
noFallthroughCasesInSwitch: true
|
|
322
|
+
},
|
|
323
|
+
include: ['src'],
|
|
324
|
+
references: [{ path: './tsconfig.node.json' }]
|
|
325
|
+
}, null, 2));
|
|
326
|
+
|
|
327
|
+
fs.writeFileSync(path.join(projectPath, 'tsconfig.node.json'),
|
|
328
|
+
JSON.stringify({
|
|
329
|
+
compilerOptions: {
|
|
330
|
+
composite: true,
|
|
331
|
+
skipLibCheck: true,
|
|
332
|
+
module: 'ESNext',
|
|
333
|
+
moduleResolution: 'bundler',
|
|
334
|
+
allowSyntheticDefaultImports: true
|
|
335
|
+
},
|
|
336
|
+
include: ['vite.config.ts']
|
|
337
|
+
}, null, 2));
|
|
338
|
+
|
|
339
|
+
// Initialize git
|
|
340
|
+
try { require('child_process').execSync('git init', { cwd: projectPath, stdio: 'ignore' }); } catch {}
|
|
341
|
+
|
|
342
|
+
return projectPath;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
createConvexClient,
|
|
347
|
+
getConvexConfig,
|
|
348
|
+
setConvexConfig,
|
|
349
|
+
query, mutation, action, subscribe,
|
|
350
|
+
signIn, signUp, signOut, getCurrentUser, resetPassword,
|
|
351
|
+
list, get, create, update, remove,
|
|
352
|
+
uploadFile, getFileUrl,
|
|
353
|
+
watchQuery,
|
|
354
|
+
scaffoldConvexProject,
|
|
355
|
+
loadConnectors, saveConnectors
|
|
356
|
+
};
|