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 +53 -0
- package/README.md +4 -29
- package/bin/loom-bun.js +2 -10
- package/bin/loom.js +0 -10
- package/package.json +6 -7
- 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 +30 -5
- package/src/core/permissions.js +12 -1
- package/src/core/plugin-cmd.js +1 -0
- package/src/core/session.js +87 -6
- package/src/tools/browser.js +204 -0
- package/src/tools/index.js +17 -1
- 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 -535
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
|
[](https://www.npmjs.com/package/loom-agent)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](#)
|
|
13
|
+
[](https://loomcluster.in/)
|
|
13
14
|
|
|
14
15
|

|
|
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
|
-
-
|
|
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-
|
|
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", "
|
|
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
|
|
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.
|
|
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/
|
|
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
|
-
"
|
|
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
|
+
};
|