loom-agent 1.2.36 → 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 CHANGED
@@ -4,6 +4,59 @@ All notable changes to **Loom Code** are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased] — completion harness, specialist agents, headless browser, TUI refresh
8
+
9
+ ### Added
10
+ - **Drive-to-completion harness (`src/core/session.js`).** When the model ends
11
+ a turn with todos still pending/in_progress (the "stops between tasks"
12
+ failure), the loop injects a continuation listing the unfinished items — up
13
+ to 5 nudges per turn, build mode only, only when the turn actually wrote
14
+ todos. Cancelled/completed items via todowrite stop the nudging cleanly.
15
+ Child subagent sessions inherit the same guard.
16
+ - **Transient provider-error retry.** Network blips / 5xx / stream resets now
17
+ retry the model call twice (side-effect free) instead of killing the turn
18
+ mid-task.
19
+ - **Specialist subagents (`src/core/agents.js`)**: `research` (multi-source
20
+ investigation, sourced report), `coder` (isolated implementation),
21
+ `reviewer` (severity-ranked code review), `tester` (reproduce → test →
22
+ green), `web` (headless-browser work). All non-delegating (`no task tool`).
23
+ - **Main orchestrator prompt.** The `build` primary agent now carries an
24
+ orchestrator role prompt (todo-first, delegate-to-specialists, verify,
25
+ finish). Primary-agent prompts are actually wired into the system prompt now
26
+ (`Session._primaryBlock()`) — previously `build`/`plan`/`chat` prompts were
27
+ registry-only and never reached the model.
28
+ - **`browser` tool (`src/tools/browser.js`).** Headless Chromium control via
29
+ Playwright: goto/click/fill/press/text/eval/links/screenshot/close. Engine:
30
+ bundled `playwright` if installed, else `playwright-core` + system
31
+ Chrome/Edge. The page singleton persists across calls. Permissions: passive
32
+ reads allow, interactive click/fill/press ask.
33
+
34
+ ### Changed
35
+ - **TUI design overhaul.** The splash leads with the full LOOM CODE wordmark
36
+ in solid terracotta over a separator rule and a pill-style status
37
+ row; the default "Loom Dark" (and Light) palettes got deeper surface
38
+ stepping so panels read without borders. Every selection surface — command
39
+ palette, slash/file popups, select modals, permission popup — now uses a
40
+ tinted active row with a `▸` marker; the command palette finally renders
41
+ each command's description; modals gained a title divider; toasts carry a
42
+ kind-colored status dot (● ok / ✖ error / ◆ info) and clip long text so
43
+ they never cover the chat. System notices sit on a tinted surface, the
44
+ assistant footer is brand-colored, markdown headings get an h1/h2/h3 color
45
+ hierarchy, the input placeholder color-codes /commands @file !shell, and
46
+ hard-coded reds now follow the theme's error color. Still grid-line-free in
47
+ the main view — color and spacing do the work, no box frames.
48
+ - **Sidebar version now tracks `package.json`** (it was stuck at a hardcoded
49
+ v1.2.0 while the package moved on); splash and sidebar share one source.
50
+ - **`task` tool description now lists every built-in subagent with its specialty.**
51
+ - **Stop messages show the real state, not canned text.** Continuation nudges
52
+ now quote the model's ACTUAL last message plus the open todo list (instead
53
+ of one identical line every time); the empty-reply nudge reports the real
54
+ tool-call count; a build turn that ends with open todos returns them
55
+ (`resp.openTodos`) and the TUI appends "Stopped with N todo(s) still open: …"
56
+ with the actual items; the interrupt line reports kept chars + the
57
+ in-progress todo; the plan-complete line includes the planned step count;
58
+ "(no response)"/"(interrupted)" placeholders describe what happened.
59
+
7
60
  ## [1.2.36] — fix `loom web` / `loom acp` / `loom attach`
8
61
 
9
62
  ### Fixed
package/README.md CHANGED
@@ -10,6 +10,7 @@
10
10
  [![npm version](https://img.shields.io/npm/v/loom-agent?style=flat&color=blue)](https://www.npmjs.com/package/loom-agent)
11
11
  [![License: MIT](https://img.shields.io/npm/l/loom-agent?style=flat&color=green)](LICENSE)
12
12
  [![Platform: Win/Mac/Linux](https://img.shields.io/badge/platform-Win--Mac--Linux-orange)](#)
13
+ [![Website: loomcluster.in](https://img.shields.io/badge/website-loomcluster.in-success)](https://loomcluster.in/)
13
14
 
14
15
  ![Loom Code splash — connected to NVIDIA](docs/screenshot.png)
15
16
 
@@ -29,7 +30,8 @@ An AI-powered coding agent for the terminal with multi-provider support and a fu
29
30
  - Session memory, /undo, /redo, /compact, /reset, /fork
30
31
  - One-shot print mode: `loom -p "query"`
31
32
  - Editor integration via the Agent Client Protocol (ACP): `loom acp` (JSON-RPC over stdio) so Zed, JetBrains, and Neovim plugins can drive Loom as their coding agent
32
- - Browser interface via `loom web` — a zero-dependency Node HTTP server (chat in the browser, password auth, CORS) plus `loom attach` to share the same server from the terminal; optional mDNS advertising as `loom.local` via [bonjour-service](https://www.npmjs.com/package/bonjour-service)
33
+ - OpenCode-style agent harness: main orchestrator agent delegates to specialist subagents (`research`, `coder`, `reviewer`, `tester`, `web`, `explore`, `scout`) via the `task` tool, and a drive-to-completion loop keeps working while todos are still open
34
+ - Headless browser tool (`browser`) — navigate, click, fill, read the DOM, and screenshot real pages from the `web` agent
33
35
  - Usage & billing tracking per session + lifetime
34
36
 
35
37
  ## Prerequisites
@@ -49,7 +51,7 @@ An AI-powered coding agent for the terminal with multi-provider support and a fu
49
51
 
50
52
  Install from npm (global — installs the `loom` command):
51
53
  ```bash
52
- npm install -g loom-code
54
+ npm install -g loom-agent
53
55
  ```
54
56
 
55
57
  Or run from source:
@@ -102,35 +104,8 @@ loom --version
102
104
  loom --help
103
105
  loom --basic # skip TUI, use line-mode REPL
104
106
  loom acp # ACP subprocess mode for editor integrations
105
- loom web # browser interface (HTTP server, opens browser)
106
- loom attach http://localhost:4096 # attach the terminal to a running loom web
107
107
  ```
108
108
 
109
- ## Web
110
-
111
- Loom runs in your browser with `loom web` — a zero-dependency HTTP server that
112
- serves a single-page UI and drives the same core session loop as the TUI. Same
113
- providers/models, tools, MCP servers, and saved sessions; chat streams live in
114
- the browser, and a shared terminal client can attach to the same server.
115
-
116
- ```bash
117
- loom web # 127.0.0.1, random port, opens the browser
118
- loom web --port 4096 # fixed port
119
- loom web --hostname 0.0.0.0 # reachable on the LAN
120
- loom web --mdns # advertise as loom.local (implies 0.0.0.0)
121
- loom web --mdns-domain proj.local # custom mDNS domain
122
- loom web --cors https://example.com
123
- LOOM_SERVER_PASSWORD=secret loom web # password-protect (user: LOOM_SERVER_USERNAME, default "loom")
124
-
125
- # Attach the terminal (shares the server's sessions/state):
126
- loom attach http://localhost:4096
127
- ```
128
-
129
- Config-file equivalent (`~/.loom/config.json`): `{ "server": { "port": 4096,
130
- "hostname": "0.0.0.0", "mdns": true, "cors": ["https://example.com"] } }` — CLI
131
- flags take precedence. Full flags, the JSON API, and `loom attach` options are
132
- documented in **[docs/web.md](docs/web.md)**.
133
-
134
109
  ## Editor integration (ACP)
135
110
 
136
111
  Loom implements the [Agent Client Protocol (ACP)](https://agentclientprotocol.com)
package/bin/loom-bun.js CHANGED
@@ -74,7 +74,7 @@ process.title = "loom-code";
74
74
  const args = process.argv.slice(2);
75
75
  const coreMode = args.includes("--basic") || args.includes("-p") || args.includes("--print")
76
76
  || args.includes("--help") || args.includes("-h") || args.includes("--version") || args.includes("-v")
77
- || ["acp", "web", "attach", "graph"].includes(args[0]);
77
+ || ["acp", "graph"].includes(args[0]);
78
78
  if (!coreMode) {
79
79
  await import("@opentui/solid/preload");
80
80
  await import("../src/tui-open.tsx");
@@ -82,19 +82,11 @@ process.title = "loom-code";
82
82
  }
83
83
  // Subcommands that must never reach the interactive CLI/TUI: cli.main()
84
84
  // has no handler for them and would fall through to launching the full
85
- // screen app (`loom web` painted the splash instead of serving HTTP).
86
- if (args[0] === "web") {
87
- require("../src/web/web-server.js").main();
88
- return;
89
- }
85
+ // screen app instead of running the requested command.
90
86
  if (args[0] === "acp") {
91
87
  require("../src/acp/acp-server.js").main();
92
88
  return;
93
89
  }
94
- if (args[0] === "attach") {
95
- require("../src/web/attach.js").main();
96
- return;
97
- }
98
90
  const { main } = require("../src/core/cli.js");
99
91
  await main();
100
92
  } catch (err) {
package/bin/loom.js CHANGED
@@ -24,16 +24,6 @@ process.on('unhandledRejection', (reason) => {
24
24
  require('../src/acp/acp-server').main();
25
25
  return;
26
26
  }
27
- if (sub === 'web') {
28
- // Browser interface: HTTP server on 127.0.0.1, opens the browser.
29
- require('../src/web/web-server').main();
30
- return;
31
- }
32
- if (sub === 'attach') {
33
- // Terminal client for a running `loom web` server.
34
- require('../src/web/attach').main();
35
- return;
36
- }
37
27
  try {
38
28
  await updateCheck();
39
29
  await main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loom-agent",
3
- "version": "1.2.36",
3
+ "version": "1.2.37",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/toshalkumbhar8979-design/loomcode.git"
@@ -24,7 +24,7 @@
24
24
  "dev": "node --watch src/index.js",
25
25
  "setup": "node src/setup.js",
26
26
  "test": "bun run src/tui/test-interactive.tsx && bun run test:unit && bun run lint:core",
27
- "test:unit": "bun test src/core/agents.test.js src/core/hooks.test.js src/core/custom-commands.test.js src/core/background-tasks.test.js src/core/memory.test.js src/core/subagent-log.test.js src/core/session.test.js src/tools/index.test.js src/tools/security.test.js src/providers/providers.test.js src/providers/caching.test.js src/providers/caching-thinking.test.js src/providers/registry.test.js src/mcp/mcp-client.test.js src/mcp/mcp-manager.test.js src/core/session-store.test.js src/skills/skills-manager.test.js src/core/usage.test.js src/core/permissions.test.js src/core/format.test.js src/acp/acp-server.test.js src/web/web-server.test.js src/tui/keybinds.test.ts",
27
+ "test:unit": "bun test src/core/agents.test.js src/core/hooks.test.js src/core/custom-commands.test.js src/core/background-tasks.test.js src/core/memory.test.js src/core/subagent-log.test.js src/core/session.test.js src/tools/index.test.js src/tools/security.test.js src/providers/providers.test.js src/providers/caching.test.js src/providers/caching-thinking.test.js src/providers/registry.test.js src/mcp/mcp-client.test.js src/mcp/mcp-manager.test.js src/core/session-store.test.js src/skills/skills-manager.test.js src/core/usage.test.js src/core/permissions.test.js src/core/format.test.js src/acp/acp-server.test.js src/tui/keybinds.test.ts",
28
28
  "lint:core": "node node_modules/typescript/bin/tsc -p tsconfig.core.json",
29
29
  "smoke:acp": "node scripts/acp-smoke.js",
30
30
  "tui": "bun bin/loom-tui.js",
@@ -38,11 +38,12 @@
38
38
  "@opentui/core": "0.5.3",
39
39
  "@opentui/solid": "0.5.3",
40
40
  "@shikijs/themes": "4.4.3",
41
- "bonjour-service": "^1.4.4",
41
+ "@supabase/supabase-js": "^2.45.0",
42
42
  "boxen": "^8.0.0",
43
43
  "chalk": "^5.3.0",
44
44
  "cli-spinners": "^3.2.0",
45
45
  "commander": "^12.1.0",
46
+ "convex": "^1.14.0",
46
47
  "dayjs": "^1.11.0",
47
48
  "diff": "^7.0.0",
48
49
  "dotenv": "^16.4.0",
@@ -50,6 +51,7 @@
50
51
  "inquirer": "^12.1.0",
51
52
  "openai": "^5.23.2",
52
53
  "ora": "^8.0.0",
54
+ "playwright-core": "^1.64.1",
53
55
  "shell-quote": "^1.8.1",
54
56
  "solid-js": "1.9.12",
55
57
  "strip-ansi": "^7.1.0",
@@ -97,10 +99,7 @@
97
99
  "LOOM.md",
98
100
  "docs/acp.md",
99
101
  "scripts/acp-smoke.js",
100
- "scripts/postinstall.js",
101
- "docs/web.md",
102
- "src/web/index.html",
103
- "src/web/graph-view.html"
102
+ "scripts/postinstall.js"
104
103
  ],
105
104
  "keywords": [
106
105
  "ai",
@@ -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
+ };