zecru-ai 0.1.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.
Files changed (41) hide show
  1. package/README.md +36 -0
  2. package/Start ZecruAI.bat +46 -0
  3. package/bin/zecru.js +498 -0
  4. package/daemon/index.ts +446 -0
  5. package/daemon/tsconfig.json +16 -0
  6. package/eslint.config.mjs +18 -0
  7. package/next.config.ts +7 -0
  8. package/package.json +38 -0
  9. package/postcss.config.mjs +7 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/manifest.json +28 -0
  13. package/public/next.svg +1 -0
  14. package/public/vercel.svg +1 -0
  15. package/public/window.svg +1 -0
  16. package/railway.json +12 -0
  17. package/server/relay.ts +182 -0
  18. package/server/tsconfig.json +16 -0
  19. package/server.ts +179 -0
  20. package/src/app/api/daemon/start/route.ts +91 -0
  21. package/src/app/api/daemon/status/route.ts +16 -0
  22. package/src/app/api/daemon/stop/route.ts +30 -0
  23. package/src/app/favicon.ico +0 -0
  24. package/src/app/globals.css +114 -0
  25. package/src/app/layout.tsx +48 -0
  26. package/src/app/page.tsx +630 -0
  27. package/src/components/ChatInput.tsx +69 -0
  28. package/src/components/ChatMessage.tsx +116 -0
  29. package/src/components/ConnectionBadge.tsx +39 -0
  30. package/src/components/ConversationSidebar.tsx +135 -0
  31. package/src/components/EmptyState.tsx +72 -0
  32. package/src/components/Header.tsx +116 -0
  33. package/src/components/PermissionCard.tsx +119 -0
  34. package/src/components/ProjectSidebar.tsx +183 -0
  35. package/src/components/SettingsPanel.tsx +578 -0
  36. package/src/components/TabSwitcher.tsx +38 -0
  37. package/src/components/TypingIndicator.tsx +61 -0
  38. package/src/hooks/useSocket.ts +232 -0
  39. package/src/types/index.ts +63 -0
  40. package/start.sh +39 -0
  41. package/tsconfig.json +40 -0
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
@@ -0,0 +1,46 @@
1
+ @echo off
2
+ title ZecruAI
3
+ color 0A
4
+
5
+ echo.
6
+ echo ========================================
7
+ echo ZecruAI - Starting up...
8
+ echo ========================================
9
+ echo.
10
+
11
+ cd /d "%~dp0"
12
+
13
+ :: Check if node is installed
14
+ where node >nul 2>nul
15
+ if %ERRORLEVEL% neq 0 (
16
+ echo [ERROR] Node.js is not installed.
17
+ echo Download it from: https://nodejs.org
18
+ echo.
19
+ pause
20
+ exit /b 1
21
+ )
22
+
23
+ :: Check if dependencies are installed
24
+ if not exist "node_modules" (
25
+ echo Installing dependencies (first time only)...
26
+ echo.
27
+ call npm install
28
+ echo.
29
+ )
30
+
31
+ :: Start browser after a short delay
32
+ start /b cmd /c "timeout /t 5 /nobreak >nul && start http://localhost:3000"
33
+
34
+ echo Starting ZecruAI...
35
+ echo.
36
+ echo App: http://localhost:3000
37
+ echo Relay: http://localhost:3001 (auto)
38
+ echo.
39
+ echo Keep this window open while using ZecruAI.
40
+ echo Press Ctrl+C to stop.
41
+ echo.
42
+ echo ========================================
43
+ echo.
44
+
45
+ :: npm run dev now starts both Next.js AND the relay
46
+ call npm run dev
package/bin/zecru.js ADDED
@@ -0,0 +1,498 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ZecruAI CLI — Connect your computer to ZecruAI
4
+ *
5
+ * Usage:
6
+ * zecru connect <code> Connect to a ZecruAI session
7
+ * zecru connect <code> --dir . Specify project directory
8
+ * zecru connect <code> --dangerous Auto-approve all actions
9
+ *
10
+ * The pairing code is shown in the ZecruAI web app under Settings.
11
+ */
12
+
13
+ "use strict";
14
+
15
+ const { spawn } = require("child_process");
16
+ const path = require("path");
17
+ const os = require("os");
18
+
19
+ // ─── Relay URL ───────────────────────────────────────────────────────
20
+ const DEFAULT_RELAY = "https://zecruai.com";
21
+ const RELAY_URL = process.env.RELAY_URL || DEFAULT_RELAY;
22
+
23
+ // ─── Colors (no dependencies) ────────────────────────────────────────
24
+ const c = {
25
+ reset: "\x1b[0m",
26
+ bold: "\x1b[1m",
27
+ dim: "\x1b[2m",
28
+ green: "\x1b[32m",
29
+ yellow: "\x1b[33m",
30
+ blue: "\x1b[34m",
31
+ magenta: "\x1b[35m",
32
+ cyan: "\x1b[36m",
33
+ red: "\x1b[31m",
34
+ gray: "\x1b[90m",
35
+ };
36
+
37
+ // ─── CLI Argument Parsing ────────────────────────────────────────────
38
+ function parseArgs() {
39
+ const args = process.argv.slice(2);
40
+ const command = args[0];
41
+
42
+ if (!command || command === "help" || command === "--help" || command === "-h") {
43
+ printHelp();
44
+ process.exit(0);
45
+ }
46
+
47
+ if (command === "version" || command === "--version" || command === "-v") {
48
+ console.log("zecru-ai v0.1.0");
49
+ process.exit(0);
50
+ }
51
+
52
+ if (command !== "connect") {
53
+ console.error(`${c.red}Unknown command: ${command}${c.reset}`);
54
+ console.error(`Run ${c.cyan}zecru help${c.reset} for usage.\n`);
55
+ process.exit(1);
56
+ }
57
+
58
+ let pairingCode = "";
59
+ let workingDir = process.cwd();
60
+ let dangerousMode = false;
61
+ let relay = RELAY_URL;
62
+
63
+ for (let i = 1; i < args.length; i++) {
64
+ const arg = args[i];
65
+ if ((arg === "--dir" || arg === "-d") && args[i + 1]) {
66
+ workingDir = path.resolve(args[++i]);
67
+ } else if ((arg === "--relay" || arg === "-r") && args[i + 1]) {
68
+ relay = args[++i];
69
+ } else if (arg === "--dangerous") {
70
+ dangerousMode = true;
71
+ } else if (!arg.startsWith("-") && !pairingCode) {
72
+ pairingCode = arg.toUpperCase();
73
+ }
74
+ }
75
+
76
+ if (!pairingCode) {
77
+ console.error(`${c.red}Missing pairing code.${c.reset}`);
78
+ console.error(`Usage: ${c.cyan}zecru connect <code>${c.reset}\n`);
79
+ process.exit(1);
80
+ }
81
+
82
+ return { pairingCode, workingDir, dangerousMode, relay };
83
+ }
84
+
85
+ function printHelp() {
86
+ console.log(`
87
+ ${c.bold}${c.cyan}ZecruAI CLI${c.reset} — Connect your computer to ZecruAI
88
+
89
+ ${c.bold}USAGE${c.reset}
90
+ ${c.cyan}zecru connect <code>${c.reset} Connect to a session
91
+ ${c.cyan}zecru connect <code> --dir <path>${c.reset} Specify project directory
92
+ ${c.cyan}zecru connect <code> --dangerous${c.reset} Auto-approve all actions
93
+
94
+ ${c.bold}OPTIONS${c.reset}
95
+ ${c.cyan}--dir, -d <path>${c.reset} Project directory (default: current directory)
96
+ ${c.cyan}--relay, -r <url>${c.reset} Relay server URL (default: ${DEFAULT_RELAY})
97
+ ${c.cyan}--dangerous${c.reset} Skip permission prompts (use with caution)
98
+
99
+ ${c.bold}EXAMPLES${c.reset}
100
+ ${c.gray}# Connect from your project folder${c.reset}
101
+ cd ~/my-project
102
+ zecru connect ABC123
103
+
104
+ ${c.gray}# Connect with explicit directory${c.reset}
105
+ zecru connect ABC123 --dir ~/my-project
106
+
107
+ ${c.bold}SETUP${c.reset}
108
+ 1. Open ${c.cyan}zecruai.com${c.reset} on your phone or browser
109
+ 2. Go to Settings and copy your pairing code
110
+ 3. Run ${c.cyan}zecru connect <code>${c.reset} on your computer
111
+
112
+ Claude Code must be authenticated. Run ${c.cyan}claude${c.reset} once to log in.
113
+ `);
114
+ }
115
+
116
+ // ─── Tool Description ────────────────────────────────────────────────
117
+ function describeToolUse(toolName, input) {
118
+ switch (toolName) {
119
+ case "Read":
120
+ return `Reading ${shortenPath(input.file_path)}`;
121
+ case "Write":
122
+ return `Writing ${shortenPath(input.file_path)}`;
123
+ case "Edit":
124
+ return `Editing ${shortenPath(input.file_path)}`;
125
+ case "Bash":
126
+ return `Running: ${truncate(input.command, 60)}`;
127
+ case "Glob":
128
+ return `Searching files: ${input.pattern}`;
129
+ case "Grep":
130
+ return `Searching code: ${truncate(input.pattern, 40)}`;
131
+ case "Task":
132
+ return `Running sub-task`;
133
+ case "WebSearch":
134
+ return `Searching web: ${truncate(input.query, 40)}`;
135
+ case "WebFetch":
136
+ return `Fetching: ${truncate(input.url, 50)}`;
137
+ default:
138
+ return `Using ${toolName}`;
139
+ }
140
+ }
141
+
142
+ function shortenPath(filePath) {
143
+ if (!filePath) return "file";
144
+ const parts = filePath.replace(/\\/g, "/").split("/");
145
+ if (parts.length <= 2) return filePath;
146
+ return `.../${parts.slice(-2).join("/")}`;
147
+ }
148
+
149
+ function truncate(text, max) {
150
+ if (!text) return "";
151
+ return text.length > max ? text.substring(0, max) + "..." : text;
152
+ }
153
+
154
+ // ─── Claude Code CLI Path ────────────────────────────────────────────
155
+ function getClaudeCLI() {
156
+ // Try local node_modules first (when running from the zecru-ai package)
157
+ const localPath = path.join(__dirname, "..", "node_modules", "@anthropic-ai", "claude-code", "cli.js");
158
+ try {
159
+ require.resolve(localPath);
160
+ return localPath;
161
+ } catch {
162
+ // Fall back to globally installed
163
+ try {
164
+ const resolved = require.resolve("@anthropic-ai/claude-code/cli.js");
165
+ return resolved;
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ }
171
+
172
+ // ─── Claude Code Bridge ─────────────────────────────────────────────
173
+ class ClaudeCodeBridge {
174
+ constructor(socket, workingDir, dangerousMode) {
175
+ this.process = null;
176
+ this.socket = socket;
177
+ this.workingDir = workingDir;
178
+ this.dangerousMode = dangerousMode;
179
+ this.conversationId = null;
180
+ this.lineBuffer = "";
181
+ }
182
+
183
+ sendMessage(content, sessionId) {
184
+ if (this.process) {
185
+ this.process.kill("SIGTERM");
186
+ this.process = null;
187
+ }
188
+
189
+ this.lineBuffer = "";
190
+
191
+ const args = [];
192
+ args.push("--print");
193
+ args.push("--verbose");
194
+ args.push("--output-format", "stream-json");
195
+
196
+ if (this.dangerousMode) {
197
+ args.push("--dangerously-skip-permissions");
198
+ }
199
+
200
+ const resumeId = sessionId || this.conversationId;
201
+ if (resumeId) {
202
+ args.push("--resume", resumeId);
203
+ }
204
+
205
+ args.push(content);
206
+
207
+ const cliPath = getClaudeCLI();
208
+ if (!cliPath) {
209
+ this.socket.emit("daemon:response", {
210
+ content: "Claude Code is not installed. Run: npm install -g @anthropic-ai/claude-code",
211
+ type: "error",
212
+ done: true,
213
+ });
214
+ return;
215
+ }
216
+
217
+ console.log(`\n ${c.cyan}[send]${c.reset} "${truncate(content, 60)}"`);
218
+
219
+ // Clean env — strip Claude Code session vars
220
+ const cleanEnv = {};
221
+ for (const [key, val] of Object.entries(process.env)) {
222
+ if (val === undefined) continue;
223
+ if (key.startsWith("CLAUDE")) continue;
224
+ if (key.startsWith("MCP")) continue;
225
+ cleanEnv[key] = val;
226
+ }
227
+
228
+ this.process = spawn(process.execPath, [cliPath, ...args], {
229
+ cwd: this.workingDir,
230
+ shell: false,
231
+ env: cleanEnv,
232
+ stdio: ["ignore", "pipe", "pipe"],
233
+ });
234
+
235
+ let gotAnyOutput = false;
236
+
237
+ this.socket.emit("daemon:activity", {
238
+ type: "status",
239
+ message: "Claude is thinking...",
240
+ });
241
+
242
+ this.process.stdout.on("data", (data) => {
243
+ gotAnyOutput = true;
244
+ this.lineBuffer += data.toString();
245
+
246
+ const lines = this.lineBuffer.split("\n");
247
+ this.lineBuffer = lines.pop() || "";
248
+
249
+ for (const line of lines) {
250
+ const trimmed = line.trim();
251
+ if (trimmed) this.processStreamLine(trimmed);
252
+ }
253
+ });
254
+
255
+ this.process.stderr.on("data", (data) => {
256
+ const text = data.toString();
257
+ console.log(` ${c.yellow}[err]${c.reset} ${text.substring(0, 200)}`);
258
+ });
259
+
260
+ this.process.on("close", (code) => {
261
+ if (this.lineBuffer.trim()) {
262
+ this.processStreamLine(this.lineBuffer.trim());
263
+ this.lineBuffer = "";
264
+ }
265
+
266
+ console.log(` ${c.dim}[done] exit code ${code}${c.reset}`);
267
+
268
+ if (!gotAnyOutput && code !== 0) {
269
+ this.socket.emit("daemon:response", {
270
+ content: `Claude Code exited with code ${code}. Make sure you're logged in — run 'claude' in a terminal once to authenticate.`,
271
+ type: "error",
272
+ done: true,
273
+ });
274
+ } else {
275
+ this.socket.emit("daemon:response", {
276
+ content: "",
277
+ type: "text",
278
+ done: true,
279
+ });
280
+ }
281
+
282
+ this.process = null;
283
+ });
284
+
285
+ this.process.on("error", (err) => {
286
+ console.error(` ${c.red}[error]${c.reset} ${err.message}`);
287
+ this.socket.emit("daemon:response", {
288
+ content: `Error starting Claude Code: ${err.message}`,
289
+ type: "error",
290
+ done: true,
291
+ });
292
+ this.process = null;
293
+ });
294
+ }
295
+
296
+ processStreamLine(line) {
297
+ let event;
298
+ try {
299
+ event = JSON.parse(line);
300
+ } catch {
301
+ return;
302
+ }
303
+
304
+ const eventType = event.type;
305
+
306
+ switch (eventType) {
307
+ case "assistant": {
308
+ const message = event.message || {};
309
+ const contentBlocks = message.content || [];
310
+
311
+ if (event.session_id) {
312
+ this.conversationId = event.session_id;
313
+ }
314
+
315
+ for (const block of contentBlocks) {
316
+ if (block.type === "text" && block.text) {
317
+ console.log(` ${c.green}[text]${c.reset} ${block.text.substring(0, 80)}`);
318
+ this.socket.emit("daemon:response", {
319
+ content: block.text,
320
+ type: "text",
321
+ done: false,
322
+ });
323
+ } else if (block.type === "tool_use") {
324
+ const description = describeToolUse(block.name, block.input || {});
325
+ console.log(` ${c.blue}[tool]${c.reset} ${description}`);
326
+ this.socket.emit("daemon:activity", {
327
+ type: "tool_use",
328
+ tool: block.name,
329
+ message: description,
330
+ input: block.input || {},
331
+ });
332
+ }
333
+ }
334
+ break;
335
+ }
336
+
337
+ case "progress": {
338
+ this.socket.emit("daemon:activity", {
339
+ type: "progress",
340
+ message: "Working...",
341
+ });
342
+ break;
343
+ }
344
+
345
+ case "system": {
346
+ console.log(` ${c.dim}[system] ${event.subtype || "unknown"}${c.reset}`);
347
+ break;
348
+ }
349
+
350
+ case "result": {
351
+ const resultText = event.result;
352
+ const isError = event.is_error;
353
+ const costUsd = event.total_cost_usd;
354
+ const durationMs = event.duration_ms;
355
+ const sessionId = event.session_id;
356
+
357
+ if (sessionId) {
358
+ this.conversationId = sessionId;
359
+ }
360
+
361
+ const costStr = costUsd != null ? `$${costUsd.toFixed(4)}` : "?";
362
+ const durationStr = durationMs != null ? `${durationMs}ms` : "?";
363
+ console.log(` ${c.magenta}[result]${c.reset} ${isError ? "ERROR" : "OK"} cost=${costStr} duration=${durationStr}`);
364
+
365
+ if (resultText) {
366
+ this.socket.emit("daemon:result", {
367
+ text: resultText,
368
+ isError: isError || false,
369
+ costUsd: costUsd || 0,
370
+ durationMs: durationMs || 0,
371
+ sessionId: sessionId || null,
372
+ });
373
+ }
374
+ break;
375
+ }
376
+ }
377
+ }
378
+
379
+ sendPermissionResponse(approved) {
380
+ console.log(` ${c.dim}[perm] ${approved ? "APPROVED" : "DENIED"}${c.reset}`);
381
+ }
382
+
383
+ kill() {
384
+ if (this.process) {
385
+ this.process.kill("SIGTERM");
386
+ this.process = null;
387
+ }
388
+ }
389
+ }
390
+
391
+ // ─── Main ────────────────────────────────────────────────────────────
392
+ async function main() {
393
+ const { pairingCode, workingDir, dangerousMode, relay } = parseArgs();
394
+
395
+ // Check Claude Code is available
396
+ const cliPath = getClaudeCLI();
397
+ if (!cliPath) {
398
+ console.error(`
399
+ ${c.red}${c.bold}Claude Code not found!${c.reset}
400
+
401
+ Install it first:
402
+ ${c.cyan}npm install -g @anthropic-ai/claude-code${c.reset}
403
+
404
+ Then authenticate (one time):
405
+ ${c.cyan}claude${c.reset}
406
+ `);
407
+ process.exit(1);
408
+ }
409
+
410
+ // Banner
411
+ console.log(`
412
+ ${c.bold}${c.cyan}ZecruAI${c.reset} ${c.dim}v0.1.0${c.reset}
413
+ ${c.dim}────────────────────────────${c.reset}
414
+ ${c.bold}Code:${c.reset} ${c.cyan}${pairingCode}${c.reset}
415
+ ${c.bold}Directory:${c.reset} ${workingDir}
416
+ ${c.bold}Relay:${c.reset} ${relay}${dangerousMode ? `\n ${c.bold}Mode:${c.reset} ${c.yellow}AUTO-APPROVE${c.reset}` : ""}
417
+ ${c.dim}────────────────────────────${c.reset}
418
+ `);
419
+
420
+ console.log(` ${c.dim}Connecting to relay...${c.reset}`);
421
+
422
+ // Load socket.io-client
423
+ let io;
424
+ try {
425
+ io = require("socket.io-client").io;
426
+ } catch {
427
+ console.error(`
428
+ ${c.red}socket.io-client not found!${c.reset}
429
+
430
+ If you installed ZecruAI globally, try reinstalling:
431
+ ${c.cyan}npm install -g zecru-ai${c.reset}
432
+
433
+ Or install the dependency manually:
434
+ ${c.cyan}npm install -g socket.io-client${c.reset}
435
+ `);
436
+ process.exit(1);
437
+ }
438
+
439
+ const socket = io(relay, {
440
+ reconnection: true,
441
+ reconnectionAttempts: Infinity,
442
+ reconnectionDelay: 2000,
443
+ timeout: 10000,
444
+ });
445
+
446
+ const bridge = new ClaudeCodeBridge(socket, workingDir, dangerousMode);
447
+
448
+ socket.on("connect", () => {
449
+ console.log(` ${c.green}Connected to relay!${c.reset}`);
450
+ socket.emit("daemon:register", {
451
+ pairingCode,
452
+ workingDir,
453
+ });
454
+ });
455
+
456
+ socket.on("daemon:registered", (data) => {
457
+ if (data.success) {
458
+ console.log(` ${c.green}${c.bold}Ready!${c.reset} Waiting for messages from ZecruAI...`);
459
+ console.log(` ${c.dim}────────────────────────────${c.reset}`);
460
+ }
461
+ });
462
+
463
+ socket.on("daemon:message", (data) => {
464
+ console.log(`\n ${c.bold}${c.cyan}━━━ New message ━━━${c.reset}`);
465
+ bridge.sendMessage(data.content, data.conversationId);
466
+ });
467
+
468
+ socket.on("daemon:permission_response", (data) => {
469
+ bridge.sendPermissionResponse(data.approved);
470
+ });
471
+
472
+ socket.on("disconnect", (reason) => {
473
+ console.log(`\n ${c.yellow}Disconnected: ${reason}. Reconnecting...${c.reset}`);
474
+ });
475
+
476
+ socket.on("connect_error", (err) => {
477
+ console.error(` ${c.red}Connection error: ${err.message}${c.reset}`);
478
+ if (err.message.includes("ECONNREFUSED")) {
479
+ console.error(` ${c.dim}Is the relay server running at ${relay}?${c.reset}`);
480
+ }
481
+ });
482
+
483
+ // Graceful shutdown
484
+ const shutdown = () => {
485
+ console.log(`\n ${c.dim}Shutting down...${c.reset}`);
486
+ bridge.kill();
487
+ socket.disconnect();
488
+ process.exit(0);
489
+ };
490
+
491
+ process.on("SIGINT", shutdown);
492
+ process.on("SIGTERM", shutdown);
493
+ }
494
+
495
+ main().catch((err) => {
496
+ console.error(`\n${c.red}Fatal error: ${err.message}${c.reset}\n`);
497
+ process.exit(1);
498
+ });