loom-agent 1.2.36 → 1.3.2

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +38 -56
  3. package/bin/loom-bun.js +2 -10
  4. package/bin/loom.js +0 -10
  5. package/package.json +126 -126
  6. package/src/connectors/convex.js +356 -0
  7. package/src/connectors/deploy.js +390 -0
  8. package/src/connectors/index.js +23 -0
  9. package/src/connectors/supabase.js +279 -0
  10. package/src/core/agents.js +32 -2
  11. package/src/core/cli.js +30 -5
  12. package/src/core/permissions.js +12 -1
  13. package/src/core/plugin-cmd.js +1 -0
  14. package/src/core/sandbox.js +400 -0
  15. package/src/core/session-store.js +245 -167
  16. package/src/core/session.js +157 -11
  17. package/src/providers/anthropic.js +10 -5
  18. package/src/providers/openai-compat.js +12 -4
  19. package/src/tools/browser.js +204 -0
  20. package/src/tools/index.js +35 -1
  21. package/src/tui/App.tsx +103 -8
  22. package/src/tui/components/BreadcrumbBar.tsx +10 -9
  23. package/src/tui/components/ChatArea.tsx +3 -3
  24. package/src/tui/components/InputBar.tsx +22 -11
  25. package/src/tui/components/MdText.tsx +2 -1
  26. package/src/tui/components/Modals.tsx +191 -17
  27. package/src/tui/components/PermissionPopup.tsx +15 -12
  28. package/src/tui/components/Sidebar.tsx +6 -2
  29. package/src/tui/components/SplashScreen.tsx +17 -24
  30. package/src/tui/components/SubagentPanel.tsx +1 -1
  31. package/src/tui/components/ToastOverlay.tsx +23 -12
  32. package/src/tui/store.ts +38 -35
  33. package/src/tui/theme.ts +45 -30
  34. package/src/tui/tool-display.ts +13 -2
  35. package/docs/web.md +0 -205
  36. package/src/web/attach.js +0 -242
  37. package/src/web/graph-view.html +0 -262
  38. package/src/web/index.html +0 -824
  39. package/src/web/web-server.js +0 -535
@@ -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
+ };