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.
- package/README.md +36 -0
- package/Start ZecruAI.bat +46 -0
- package/bin/zecru.js +498 -0
- package/daemon/index.ts +446 -0
- package/daemon/tsconfig.json +16 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +7 -0
- package/package.json +38 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/manifest.json +28 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/railway.json +12 -0
- package/server/relay.ts +182 -0
- package/server/tsconfig.json +16 -0
- package/server.ts +179 -0
- package/src/app/api/daemon/start/route.ts +91 -0
- package/src/app/api/daemon/status/route.ts +16 -0
- package/src/app/api/daemon/stop/route.ts +30 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +114 -0
- package/src/app/layout.tsx +48 -0
- package/src/app/page.tsx +630 -0
- package/src/components/ChatInput.tsx +69 -0
- package/src/components/ChatMessage.tsx +116 -0
- package/src/components/ConnectionBadge.tsx +39 -0
- package/src/components/ConversationSidebar.tsx +135 -0
- package/src/components/EmptyState.tsx +72 -0
- package/src/components/Header.tsx +116 -0
- package/src/components/PermissionCard.tsx +119 -0
- package/src/components/ProjectSidebar.tsx +183 -0
- package/src/components/SettingsPanel.tsx +578 -0
- package/src/components/TabSwitcher.tsx +38 -0
- package/src/components/TypingIndicator.tsx +61 -0
- package/src/hooks/useSocket.ts +232 -0
- package/src/types/index.ts +63 -0
- package/start.sh +39 -0
- 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
|
+
});
|