claude-space 2.0.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 +165 -0
- package/bin/claude-space +122 -0
- package/bin/claude-space.js +59 -0
- package/out/main/index.js +3830 -0
- package/out/preload/index.js +250 -0
- package/out/renderer/assets/claudeFolderGenerator-BgGHew94.js +179 -0
- package/out/renderer/assets/index-C65XSohL.js +93249 -0
- package/out/renderer/assets/index-DEj3pJkQ.css +12262 -0
- package/out/renderer/index.html +20 -0
- package/package.json +81 -0
- package/resources/icon.png +0 -0
- package/scripts/create-release-assets.sh +54 -0
- package/scripts/postinstall.js +143 -0
|
@@ -0,0 +1,3830 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
const electron = require("electron");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const url = require("url");
|
|
28
|
+
const utils = require("@electron-toolkit/utils");
|
|
29
|
+
const yaml = require("js-yaml");
|
|
30
|
+
const crypto = require("crypto");
|
|
31
|
+
const uuid = require("uuid");
|
|
32
|
+
const child_process = require("child_process");
|
|
33
|
+
const icon = path.join(__dirname, "../../resources/icon.png");
|
|
34
|
+
class AuthSettingsManager {
|
|
35
|
+
settingsPath;
|
|
36
|
+
settings = null;
|
|
37
|
+
constructor() {
|
|
38
|
+
this.settingsPath = path.join(electron.app.getPath("userData"), "auth-settings.json");
|
|
39
|
+
}
|
|
40
|
+
async loadSettings() {
|
|
41
|
+
if (this.settings) {
|
|
42
|
+
return this.settings;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const content = await fs.promises.readFile(this.settingsPath, "utf-8");
|
|
46
|
+
this.settings = JSON.parse(content);
|
|
47
|
+
return this.settings;
|
|
48
|
+
} catch {
|
|
49
|
+
this.settings = {
|
|
50
|
+
preferredMethod: "cli",
|
|
51
|
+
autoFallback: true
|
|
52
|
+
};
|
|
53
|
+
return this.settings;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async saveSettings(settings) {
|
|
57
|
+
this.settings = settings;
|
|
58
|
+
await fs.promises.writeFile(this.settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
59
|
+
}
|
|
60
|
+
async getPreferredMethod(projectPath) {
|
|
61
|
+
const settings = await this.loadSettings();
|
|
62
|
+
if (projectPath && settings.projectOverrides?.[projectPath]) {
|
|
63
|
+
return settings.projectOverrides[projectPath].preferredMethod;
|
|
64
|
+
}
|
|
65
|
+
return settings.preferredMethod;
|
|
66
|
+
}
|
|
67
|
+
async setPreferredMethod(method, projectPath) {
|
|
68
|
+
const settings = await this.loadSettings();
|
|
69
|
+
if (projectPath) {
|
|
70
|
+
if (!settings.projectOverrides) {
|
|
71
|
+
settings.projectOverrides = {};
|
|
72
|
+
}
|
|
73
|
+
settings.projectOverrides[projectPath] = { preferredMethod: method };
|
|
74
|
+
} else {
|
|
75
|
+
settings.preferredMethod = method;
|
|
76
|
+
}
|
|
77
|
+
await this.saveSettings(settings);
|
|
78
|
+
}
|
|
79
|
+
async getAutoFallback() {
|
|
80
|
+
const settings = await this.loadSettings();
|
|
81
|
+
return settings.autoFallback;
|
|
82
|
+
}
|
|
83
|
+
async setAutoFallback(enabled) {
|
|
84
|
+
const settings = await this.loadSettings();
|
|
85
|
+
settings.autoFallback = enabled;
|
|
86
|
+
await this.saveSettings(settings);
|
|
87
|
+
}
|
|
88
|
+
async getCustomCliCommand() {
|
|
89
|
+
const settings = await this.loadSettings();
|
|
90
|
+
const command = settings.customCliCommand;
|
|
91
|
+
return command && command.trim() ? command.trim() : null;
|
|
92
|
+
}
|
|
93
|
+
async setCustomCliCommand(command) {
|
|
94
|
+
const settings = await this.loadSettings();
|
|
95
|
+
settings.customCliCommand = command.trim();
|
|
96
|
+
await this.saveSettings(settings);
|
|
97
|
+
}
|
|
98
|
+
async clearCustomCliCommand() {
|
|
99
|
+
const settings = await this.loadSettings();
|
|
100
|
+
delete settings.customCliCommand;
|
|
101
|
+
await this.saveSettings(settings);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const authSettingsManager = new AuthSettingsManager();
|
|
105
|
+
class AgentManager {
|
|
106
|
+
sessions = /* @__PURE__ */ new Map();
|
|
107
|
+
sdkModule = null;
|
|
108
|
+
/**
|
|
109
|
+
* Load the Claude SDK dynamically (ES module)
|
|
110
|
+
*/
|
|
111
|
+
async loadSDK() {
|
|
112
|
+
if (!this.sdkModule) {
|
|
113
|
+
this.sdkModule = await import("@anthropic-ai/claude-agent-sdk");
|
|
114
|
+
}
|
|
115
|
+
return this.sdkModule;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Create a new agent session
|
|
119
|
+
* Returns a session ID for future message sending
|
|
120
|
+
*/
|
|
121
|
+
async createSession(config) {
|
|
122
|
+
const sessionId = crypto.randomUUID();
|
|
123
|
+
const abortController = new AbortController();
|
|
124
|
+
this.sessions.set(sessionId, {
|
|
125
|
+
config,
|
|
126
|
+
abortController,
|
|
127
|
+
isActive: true,
|
|
128
|
+
sdkSessionId: config.sdkSessionId,
|
|
129
|
+
// Use provided SDK session ID if available
|
|
130
|
+
pendingApprovals: /* @__PURE__ */ new Map()
|
|
131
|
+
// Initialize empty approval map
|
|
132
|
+
});
|
|
133
|
+
return sessionId;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Send a message and stream the response
|
|
137
|
+
* Calls onChunk for each piece of content
|
|
138
|
+
*/
|
|
139
|
+
async sendMessage(sessionId, message, onChunk) {
|
|
140
|
+
const session = this.sessions.get(sessionId);
|
|
141
|
+
if (!session) {
|
|
142
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
143
|
+
}
|
|
144
|
+
if (!session.isActive) {
|
|
145
|
+
throw new Error(`Session ${sessionId} is not active`);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const sdk = await this.loadSDK();
|
|
149
|
+
const { query } = sdk;
|
|
150
|
+
const originalCwd = process.cwd();
|
|
151
|
+
let agentPath = session.config.projectPath;
|
|
152
|
+
if (session.config.agentFolderName) {
|
|
153
|
+
const candidate = `${session.config.projectPath}/${session.config.agentFolderName}`;
|
|
154
|
+
if (fs.existsSync(candidate)) {
|
|
155
|
+
agentPath = candidate;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
process.chdir(agentPath);
|
|
159
|
+
let mcpServers;
|
|
160
|
+
const mcpConfigPath = path.join(session.config.projectPath, ".mcp.json");
|
|
161
|
+
console.log("[MCP] Looking for .mcp.json at:", mcpConfigPath);
|
|
162
|
+
if (fs.existsSync(mcpConfigPath)) {
|
|
163
|
+
try {
|
|
164
|
+
const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
|
|
165
|
+
if (mcpConfig.mcpServers) {
|
|
166
|
+
mcpServers = mcpConfig.mcpServers;
|
|
167
|
+
console.log("[MCP] Loaded MCP servers:", Object.keys(mcpServers || {}));
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error("[MCP] Failed to load .mcp.json:", err);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
console.log("[MCP] No .mcp.json found");
|
|
174
|
+
}
|
|
175
|
+
const customCliCommand = await authSettingsManager.getCustomCliCommand();
|
|
176
|
+
const queryOptions = {
|
|
177
|
+
// Only include apiKey if authMethod is 'apikey' and key is provided
|
|
178
|
+
// When omitted, SDK will use CLI authentication automatically
|
|
179
|
+
...session.config.authMethod === "apikey" && session.config.apiKey ? { apiKey: session.config.apiKey } : {},
|
|
180
|
+
settingSources: ["project", "local"],
|
|
181
|
+
// Load settings.json (skills) and settings.local.json
|
|
182
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
183
|
+
// Load CLAUDE.md
|
|
184
|
+
abortController: session.abortController,
|
|
185
|
+
permissionMode: session.config.permissionMode || "default",
|
|
186
|
+
// Use configured mode or default
|
|
187
|
+
...mcpServers ? { mcpServers } : {},
|
|
188
|
+
// Include MCP servers if loaded
|
|
189
|
+
// Use custom CLI command if set, otherwise use default CLI path
|
|
190
|
+
...customCliCommand ? {
|
|
191
|
+
pathToClaudeCodeExecutable: customCliCommand
|
|
192
|
+
} : electron.app.isPackaged ? {
|
|
193
|
+
// In production builds, explicitly point to the unpacked CLI
|
|
194
|
+
pathToClaudeCodeExecutable: path.join(
|
|
195
|
+
process.resourcesPath,
|
|
196
|
+
"app.asar.unpacked",
|
|
197
|
+
"node_modules",
|
|
198
|
+
"@anthropic-ai",
|
|
199
|
+
"claude-agent-sdk",
|
|
200
|
+
"cli.js"
|
|
201
|
+
)
|
|
202
|
+
} : {},
|
|
203
|
+
// Custom permission handler for human-in-the-loop approval
|
|
204
|
+
canUseTool: async (toolName, input, options) => {
|
|
205
|
+
console.log("🔐 [APPROVAL] canUseTool called for:", toolName);
|
|
206
|
+
console.log("🔐 [APPROVAL] Tool input:", JSON.stringify(input, null, 2));
|
|
207
|
+
console.log("🔐 [APPROVAL] Options:", options);
|
|
208
|
+
const requestId = crypto.randomUUID();
|
|
209
|
+
console.log("🔐 [APPROVAL] Generated requestId:", requestId);
|
|
210
|
+
const approvalPromise = new Promise((resolve, reject) => {
|
|
211
|
+
session.pendingApprovals.set(requestId, { requestId, resolve, reject });
|
|
212
|
+
console.log("🔐 [APPROVAL] Added to pending approvals, count:", session.pendingApprovals.size);
|
|
213
|
+
});
|
|
214
|
+
console.log("🔐 [APPROVAL] Sending approval request to frontend...");
|
|
215
|
+
onChunk({
|
|
216
|
+
type: "approval_request",
|
|
217
|
+
approvalRequest: {
|
|
218
|
+
requestId,
|
|
219
|
+
toolName,
|
|
220
|
+
parameters: input
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
console.log("🔐 [APPROVAL] Chunk sent, waiting for user response...");
|
|
224
|
+
const approved = await approvalPromise;
|
|
225
|
+
console.log("🔐 [APPROVAL] User response received:", approved ? "APPROVED" : "DENIED");
|
|
226
|
+
if (approved) {
|
|
227
|
+
console.log("🔐 [APPROVAL] Returning allow result");
|
|
228
|
+
const result2 = {
|
|
229
|
+
behavior: "allow",
|
|
230
|
+
updatedInput: input,
|
|
231
|
+
toolUseID: options.toolUseID
|
|
232
|
+
};
|
|
233
|
+
return result2;
|
|
234
|
+
} else {
|
|
235
|
+
console.log("🔐 [APPROVAL] Returning deny result");
|
|
236
|
+
const result2 = {
|
|
237
|
+
behavior: "deny",
|
|
238
|
+
message: "User denied tool execution",
|
|
239
|
+
interrupt: true,
|
|
240
|
+
toolUseID: options.toolUseID
|
|
241
|
+
};
|
|
242
|
+
return result2;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
if (session.sdkSessionId) {
|
|
247
|
+
queryOptions.resume = session.sdkSessionId;
|
|
248
|
+
}
|
|
249
|
+
const result = query({
|
|
250
|
+
prompt: message,
|
|
251
|
+
options: queryOptions
|
|
252
|
+
});
|
|
253
|
+
for await (const msg of result) {
|
|
254
|
+
if (!session.isActive) {
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (msg.type === "system" && msg.subtype === "init" && msg.session_id && !session.sdkSessionId) {
|
|
258
|
+
session.sdkSessionId = msg.session_id;
|
|
259
|
+
onChunk({
|
|
260
|
+
type: "session_id",
|
|
261
|
+
sdkSessionId: msg.session_id
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
switch (msg.type) {
|
|
265
|
+
case "assistant":
|
|
266
|
+
if (msg.message?.content) {
|
|
267
|
+
const content = Array.isArray(msg.message.content) ? msg.message.content : [msg.message.content];
|
|
268
|
+
let fullText = "";
|
|
269
|
+
for (const block of content) {
|
|
270
|
+
if (typeof block === "string") {
|
|
271
|
+
onChunk({ type: "content", content: block });
|
|
272
|
+
fullText += block;
|
|
273
|
+
} else if (block.type === "text") {
|
|
274
|
+
onChunk({ type: "content", content: block.text });
|
|
275
|
+
fullText += block.text;
|
|
276
|
+
} else if (block.type === "tool_use") {
|
|
277
|
+
onChunk({
|
|
278
|
+
type: "tool_use",
|
|
279
|
+
tool: block.name,
|
|
280
|
+
content: JSON.stringify(block.input, null, 2)
|
|
281
|
+
});
|
|
282
|
+
onChunk({
|
|
283
|
+
type: "step",
|
|
284
|
+
step: {
|
|
285
|
+
type: "tool_call",
|
|
286
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
287
|
+
title: `Tool: ${block.name}`,
|
|
288
|
+
tool: block.name,
|
|
289
|
+
input: block.input,
|
|
290
|
+
status: "running"
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (fullText) {
|
|
296
|
+
onChunk({
|
|
297
|
+
type: "step",
|
|
298
|
+
step: {
|
|
299
|
+
type: "llm_call",
|
|
300
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
301
|
+
title: "LLM Call",
|
|
302
|
+
messages: [{ role: "user", content: message }],
|
|
303
|
+
response: fullText,
|
|
304
|
+
duration: 0
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
case "result":
|
|
311
|
+
if (msg.subtype === "success") {
|
|
312
|
+
onChunk({
|
|
313
|
+
type: "step",
|
|
314
|
+
step: {
|
|
315
|
+
type: "end",
|
|
316
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
317
|
+
title: "End",
|
|
318
|
+
totalCost: msg.total_cost_usd || 0,
|
|
319
|
+
totalTokens: (msg.usage?.input_tokens || 0) + (msg.usage?.output_tokens || 0)
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
onChunk({
|
|
323
|
+
type: "metadata",
|
|
324
|
+
metadata: {
|
|
325
|
+
cost: msg.total_cost_usd || 0,
|
|
326
|
+
tokens: {
|
|
327
|
+
input: msg.usage?.input_tokens || 0,
|
|
328
|
+
output: msg.usage?.output_tokens || 0,
|
|
329
|
+
total: (msg.usage?.input_tokens || 0) + (msg.usage?.output_tokens || 0)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
} else if (msg.subtype === "error") {
|
|
334
|
+
onChunk({
|
|
335
|
+
type: "error",
|
|
336
|
+
error: msg.error?.message || "Unknown error occurred"
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
break;
|
|
340
|
+
case "user":
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
console.log("Unhandled message type:", msg.type);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
process.chdir(originalCwd);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
349
|
+
onChunk({
|
|
350
|
+
type: "error",
|
|
351
|
+
error: errorMessage
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Update session configuration (e.g. permission mode)
|
|
357
|
+
*/
|
|
358
|
+
updateSessionConfig(sessionId, updates) {
|
|
359
|
+
const session = this.sessions.get(sessionId);
|
|
360
|
+
if (session) {
|
|
361
|
+
session.config = { ...session.config, ...updates };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Stop an active session
|
|
366
|
+
*/
|
|
367
|
+
stopSession(sessionId) {
|
|
368
|
+
const session = this.sessions.get(sessionId);
|
|
369
|
+
if (session) {
|
|
370
|
+
session.isActive = false;
|
|
371
|
+
session.abortController.abort();
|
|
372
|
+
this.sessions.delete(sessionId);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Stop all sessions
|
|
377
|
+
*/
|
|
378
|
+
stopAllSessions() {
|
|
379
|
+
for (const [sessionId] of this.sessions) {
|
|
380
|
+
this.stopSession(sessionId);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Check if a session exists and is active
|
|
385
|
+
*/
|
|
386
|
+
isSessionActive(sessionId) {
|
|
387
|
+
const session = this.sessions.get(sessionId);
|
|
388
|
+
return session?.isActive ?? false;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Get active session count
|
|
392
|
+
*/
|
|
393
|
+
getActiveSessionCount() {
|
|
394
|
+
return Array.from(this.sessions.values()).filter((s) => s.isActive).length;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Respond to a pending approval request
|
|
398
|
+
*/
|
|
399
|
+
respondToApproval(sessionId, requestId, approved) {
|
|
400
|
+
console.log("🔐 [APPROVAL] respondToApproval called:", { sessionId, requestId, approved });
|
|
401
|
+
const session = this.sessions.get(sessionId);
|
|
402
|
+
if (!session) {
|
|
403
|
+
console.error("🔐 [APPROVAL] Session not found:", sessionId);
|
|
404
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
405
|
+
}
|
|
406
|
+
console.log("🔐 [APPROVAL] Pending approvals count:", session.pendingApprovals.size);
|
|
407
|
+
console.log("🔐 [APPROVAL] Pending approval IDs:", Array.from(session.pendingApprovals.keys()));
|
|
408
|
+
const pendingApproval = session.pendingApprovals.get(requestId);
|
|
409
|
+
if (!pendingApproval) {
|
|
410
|
+
console.error("🔐 [APPROVAL] Request ID not found:", requestId);
|
|
411
|
+
throw new Error(`Approval request ${requestId} not found`);
|
|
412
|
+
}
|
|
413
|
+
console.log("🔐 [APPROVAL] Resolving approval promise with:", approved);
|
|
414
|
+
pendingApproval.resolve(approved);
|
|
415
|
+
session.pendingApprovals.delete(requestId);
|
|
416
|
+
console.log("🔐 [APPROVAL] Cleanup complete, remaining count:", session.pendingApprovals.size);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const agentManager = new AgentManager();
|
|
420
|
+
class KeyManager {
|
|
421
|
+
keysPath;
|
|
422
|
+
keyStore = null;
|
|
423
|
+
constructor() {
|
|
424
|
+
this.keysPath = path.join(electron.app.getPath("userData"), "keys.json");
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Load the key store from disk
|
|
428
|
+
*/
|
|
429
|
+
async loadKeyStore() {
|
|
430
|
+
if (this.keyStore) {
|
|
431
|
+
return this.keyStore;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const content = await fs.promises.readFile(this.keysPath, "utf-8");
|
|
435
|
+
this.keyStore = JSON.parse(content);
|
|
436
|
+
return this.keyStore;
|
|
437
|
+
} catch {
|
|
438
|
+
this.keyStore = { global: {}, projects: {} };
|
|
439
|
+
return this.keyStore;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Save the key store to disk
|
|
444
|
+
*/
|
|
445
|
+
async saveKeyStore(store) {
|
|
446
|
+
this.keyStore = store;
|
|
447
|
+
await fs.promises.writeFile(this.keysPath, JSON.stringify(store, null, 2), "utf-8");
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Encrypt a string using Electron's safeStorage
|
|
451
|
+
*/
|
|
452
|
+
encrypt(plaintext) {
|
|
453
|
+
if (!electron.safeStorage.isEncryptionAvailable()) {
|
|
454
|
+
console.warn("Encryption not available, storing key as base64");
|
|
455
|
+
return Buffer.from(plaintext).toString("base64");
|
|
456
|
+
}
|
|
457
|
+
const buffer = electron.safeStorage.encryptString(plaintext);
|
|
458
|
+
return buffer.toString("base64");
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Decrypt a string using Electron's safeStorage
|
|
462
|
+
*/
|
|
463
|
+
decrypt(encrypted) {
|
|
464
|
+
if (!electron.safeStorage.isEncryptionAvailable()) {
|
|
465
|
+
console.warn("Encryption not available, decoding from base64");
|
|
466
|
+
return Buffer.from(encrypted, "base64").toString("utf-8");
|
|
467
|
+
}
|
|
468
|
+
const buffer = Buffer.from(encrypted, "base64");
|
|
469
|
+
return electron.safeStorage.decryptString(buffer);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Get Anthropic API key
|
|
473
|
+
* Checks project-specific key first, then falls back to global key
|
|
474
|
+
*/
|
|
475
|
+
async getAnthropicKey(projectPath) {
|
|
476
|
+
const store = await this.loadKeyStore();
|
|
477
|
+
if (projectPath && store.projects?.[projectPath]?.anthropic) {
|
|
478
|
+
try {
|
|
479
|
+
return this.decrypt(store.projects[projectPath].anthropic);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
console.error("Error decrypting project-specific key:", error);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (store.global?.anthropic) {
|
|
485
|
+
try {
|
|
486
|
+
return this.decrypt(store.global.anthropic);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.error("Error decrypting global key:", error);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Set Anthropic API key
|
|
495
|
+
* If projectPath is provided, stores as project-specific key
|
|
496
|
+
* Otherwise, stores as global key
|
|
497
|
+
*/
|
|
498
|
+
async setAnthropicKey(key, projectPath) {
|
|
499
|
+
const store = await this.loadKeyStore();
|
|
500
|
+
const encrypted = this.encrypt(key);
|
|
501
|
+
if (projectPath) {
|
|
502
|
+
if (!store.projects) {
|
|
503
|
+
store.projects = {};
|
|
504
|
+
}
|
|
505
|
+
if (!store.projects[projectPath]) {
|
|
506
|
+
store.projects[projectPath] = {};
|
|
507
|
+
}
|
|
508
|
+
store.projects[projectPath].anthropic = encrypted;
|
|
509
|
+
} else {
|
|
510
|
+
if (!store.global) {
|
|
511
|
+
store.global = {};
|
|
512
|
+
}
|
|
513
|
+
store.global.anthropic = encrypted;
|
|
514
|
+
}
|
|
515
|
+
await this.saveKeyStore(store);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Check if any Anthropic API key exists
|
|
519
|
+
*/
|
|
520
|
+
async hasAnthropicKey(projectPath) {
|
|
521
|
+
const key = await this.getAnthropicKey(projectPath);
|
|
522
|
+
return key !== null && key.length > 0;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Delete Anthropic API key
|
|
526
|
+
*/
|
|
527
|
+
async deleteAnthropicKey(projectPath) {
|
|
528
|
+
const store = await this.loadKeyStore();
|
|
529
|
+
if (projectPath) {
|
|
530
|
+
if (store.projects?.[projectPath]?.anthropic) {
|
|
531
|
+
delete store.projects[projectPath].anthropic;
|
|
532
|
+
if (Object.keys(store.projects[projectPath]).length === 0) {
|
|
533
|
+
delete store.projects[projectPath];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
if (store.global?.anthropic) {
|
|
538
|
+
delete store.global.anthropic;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
await this.saveKeyStore(store);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Clear all stored keys (useful for debugging/testing)
|
|
545
|
+
*/
|
|
546
|
+
async clearAllKeys() {
|
|
547
|
+
this.keyStore = { global: {}, projects: {} };
|
|
548
|
+
await this.saveKeyStore(this.keyStore);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const keyManager = new KeyManager();
|
|
552
|
+
class LogManager {
|
|
553
|
+
/**
|
|
554
|
+
* Ensure the .logs folder exists in the project
|
|
555
|
+
*/
|
|
556
|
+
async ensureLogsFolder(projectPath) {
|
|
557
|
+
const logsPath = path.join(projectPath, ".logs");
|
|
558
|
+
try {
|
|
559
|
+
await fs.promises.mkdir(logsPath, { recursive: true });
|
|
560
|
+
return logsPath;
|
|
561
|
+
} catch (error) {
|
|
562
|
+
console.error("Error creating logs folder:", error);
|
|
563
|
+
throw error;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Get the path to the history.json file
|
|
568
|
+
*/
|
|
569
|
+
async getHistoryFilePath(projectPath) {
|
|
570
|
+
const logsPath = await this.ensureLogsFolder(projectPath);
|
|
571
|
+
return path.join(logsPath, "history.json");
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Load conversation logs from history.json
|
|
575
|
+
*/
|
|
576
|
+
async loadLogs(projectPath) {
|
|
577
|
+
try {
|
|
578
|
+
const historyPath = await this.getHistoryFilePath(projectPath);
|
|
579
|
+
try {
|
|
580
|
+
const content = await fs.promises.readFile(historyPath, "utf-8");
|
|
581
|
+
return JSON.parse(content);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (error.code === "ENOENT") {
|
|
584
|
+
return [];
|
|
585
|
+
}
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.error("Error loading logs:", error);
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Save conversation logs to history.json
|
|
595
|
+
*/
|
|
596
|
+
async saveLogs(projectPath, logs) {
|
|
597
|
+
try {
|
|
598
|
+
const historyPath = await this.getHistoryFilePath(projectPath);
|
|
599
|
+
await fs.promises.writeFile(historyPath, JSON.stringify(logs, null, 2), "utf-8");
|
|
600
|
+
} catch (error) {
|
|
601
|
+
console.error("Error saving logs:", error);
|
|
602
|
+
throw error;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const logManager = new LogManager();
|
|
607
|
+
const getLibraryPath = () => path.join(electron.app.getPath("userData"), "node-library");
|
|
608
|
+
const ensureLibraryExists = async () => {
|
|
609
|
+
const libraryPath = getLibraryPath();
|
|
610
|
+
await fs.promises.mkdir(libraryPath, { recursive: true });
|
|
611
|
+
await fs.promises.mkdir(path.join(libraryPath, "skills"), { recursive: true });
|
|
612
|
+
await fs.promises.mkdir(path.join(libraryPath, "memory"), { recursive: true });
|
|
613
|
+
return libraryPath;
|
|
614
|
+
};
|
|
615
|
+
const loadJsonFile = async (fileName) => {
|
|
616
|
+
const libraryPath = getLibraryPath();
|
|
617
|
+
const filePath = path.join(libraryPath, fileName);
|
|
618
|
+
try {
|
|
619
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
620
|
+
return JSON.parse(content);
|
|
621
|
+
} catch {
|
|
622
|
+
return { version: "1.0.0", items: [] };
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
const saveJsonFile = async (fileName, data) => {
|
|
626
|
+
const libraryPath = await ensureLibraryExists();
|
|
627
|
+
const filePath = path.join(libraryPath, fileName);
|
|
628
|
+
await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
629
|
+
};
|
|
630
|
+
const createJsonCrudHandlers = (typeName, fileName, defaultColor) => {
|
|
631
|
+
electron.ipcMain.handle(`nodeLibrary:${typeName}:list`, async () => {
|
|
632
|
+
try {
|
|
633
|
+
const data = await loadJsonFile(fileName);
|
|
634
|
+
return { success: true, items: data.items };
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error(`Error listing ${typeName}:`, error);
|
|
637
|
+
throw new Error(error.message || `Failed to list ${typeName}`);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
electron.ipcMain.handle(`nodeLibrary:${typeName}:get`, async (_, id) => {
|
|
641
|
+
try {
|
|
642
|
+
const data = await loadJsonFile(fileName);
|
|
643
|
+
const item = data.items.find((i) => i.id === id);
|
|
644
|
+
if (!item) {
|
|
645
|
+
throw new Error(`${typeName} not found`);
|
|
646
|
+
}
|
|
647
|
+
return { success: true, item };
|
|
648
|
+
} catch (error) {
|
|
649
|
+
console.error(`Error getting ${typeName}:`, error);
|
|
650
|
+
throw new Error(error.message || `Failed to get ${typeName}`);
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
electron.ipcMain.handle(`nodeLibrary:${typeName}:create`, async (_, itemData) => {
|
|
654
|
+
try {
|
|
655
|
+
await ensureLibraryExists();
|
|
656
|
+
const data = await loadJsonFile(fileName);
|
|
657
|
+
const newItem = {
|
|
658
|
+
id: uuid.v4(),
|
|
659
|
+
color: defaultColor,
|
|
660
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
661
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
662
|
+
...itemData
|
|
663
|
+
};
|
|
664
|
+
data.items.push(newItem);
|
|
665
|
+
await saveJsonFile(fileName, data);
|
|
666
|
+
return { success: true, item: newItem };
|
|
667
|
+
} catch (error) {
|
|
668
|
+
console.error(`Error creating ${typeName}:`, error);
|
|
669
|
+
throw new Error(error.message || `Failed to create ${typeName}`);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
electron.ipcMain.handle(`nodeLibrary:${typeName}:update`, async (_, id, updates) => {
|
|
673
|
+
try {
|
|
674
|
+
const data = await loadJsonFile(fileName);
|
|
675
|
+
const index = data.items.findIndex((i) => i.id === id);
|
|
676
|
+
if (index === -1) {
|
|
677
|
+
throw new Error(`${typeName} not found`);
|
|
678
|
+
}
|
|
679
|
+
const { id: _id, ...editableUpdates } = updates;
|
|
680
|
+
updates = editableUpdates;
|
|
681
|
+
data.items[index] = {
|
|
682
|
+
...data.items[index],
|
|
683
|
+
...updates,
|
|
684
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
685
|
+
};
|
|
686
|
+
await saveJsonFile(fileName, data);
|
|
687
|
+
return { success: true, item: data.items[index] };
|
|
688
|
+
} catch (error) {
|
|
689
|
+
console.error(`Error updating ${typeName}:`, error);
|
|
690
|
+
throw new Error(error.message || `Failed to update ${typeName}`);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
electron.ipcMain.handle(`nodeLibrary:${typeName}:delete`, async (_, id) => {
|
|
694
|
+
try {
|
|
695
|
+
const data = await loadJsonFile(fileName);
|
|
696
|
+
const item = data.items.find((i) => i.id === id);
|
|
697
|
+
if (!item) {
|
|
698
|
+
throw new Error(`${typeName} not found`);
|
|
699
|
+
}
|
|
700
|
+
data.items = data.items.filter((i) => i.id !== id);
|
|
701
|
+
await saveJsonFile(fileName, data);
|
|
702
|
+
return { success: true };
|
|
703
|
+
} catch (error) {
|
|
704
|
+
console.error(`Error deleting ${typeName}:`, error);
|
|
705
|
+
throw new Error(error.message || `Failed to delete ${typeName}`);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
};
|
|
709
|
+
const registerSkillHandlers = () => {
|
|
710
|
+
const skillsFolder = () => path.join(getLibraryPath(), "skills");
|
|
711
|
+
electron.ipcMain.handle("nodeLibrary:skill:list", async () => {
|
|
712
|
+
try {
|
|
713
|
+
await ensureLibraryExists();
|
|
714
|
+
const skillsPath = skillsFolder();
|
|
715
|
+
let entries = [];
|
|
716
|
+
try {
|
|
717
|
+
entries = await fs.promises.readdir(skillsPath, { withFileTypes: true });
|
|
718
|
+
} catch {
|
|
719
|
+
return { success: true, items: [] };
|
|
720
|
+
}
|
|
721
|
+
const skills = [];
|
|
722
|
+
for (const entry of entries) {
|
|
723
|
+
if (entry.isDirectory()) {
|
|
724
|
+
const skillPath = path.join(skillsPath, entry.name);
|
|
725
|
+
const skillMdPath = path.join(skillPath, "SKILL.md");
|
|
726
|
+
let name = entry.name;
|
|
727
|
+
let description = "";
|
|
728
|
+
try {
|
|
729
|
+
const content = await fs.promises.readFile(skillMdPath, "utf-8");
|
|
730
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
731
|
+
if (frontmatterMatch) {
|
|
732
|
+
const lines = frontmatterMatch[1].split("\n");
|
|
733
|
+
for (const line of lines) {
|
|
734
|
+
const [key, ...valueParts] = line.split(":");
|
|
735
|
+
const value = valueParts.join(":").trim();
|
|
736
|
+
if (key.trim() === "name") name = value;
|
|
737
|
+
if (key.trim() === "description") description = value;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
const stats = await fs.promises.stat(skillPath);
|
|
743
|
+
skills.push({
|
|
744
|
+
id: entry.name,
|
|
745
|
+
// Use folder name as ID
|
|
746
|
+
name,
|
|
747
|
+
folderName: entry.name,
|
|
748
|
+
icon: "/Sk",
|
|
749
|
+
description,
|
|
750
|
+
color: "#ff5252",
|
|
751
|
+
createdAt: stats.birthtime.toISOString(),
|
|
752
|
+
updatedAt: stats.mtime.toISOString()
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return { success: true, items: skills };
|
|
757
|
+
} catch (error) {
|
|
758
|
+
console.error("Error listing skills:", error);
|
|
759
|
+
throw new Error(error.message || "Failed to list skills");
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
electron.ipcMain.handle("nodeLibrary:skill:get", async (_, id) => {
|
|
763
|
+
try {
|
|
764
|
+
const skillPath = path.join(skillsFolder(), id);
|
|
765
|
+
const skillMdPath = path.join(skillPath, "SKILL.md");
|
|
766
|
+
await fs.promises.access(skillPath);
|
|
767
|
+
let skillMdContent = "";
|
|
768
|
+
let name = id;
|
|
769
|
+
let description = "";
|
|
770
|
+
try {
|
|
771
|
+
skillMdContent = await fs.promises.readFile(skillMdPath, "utf-8");
|
|
772
|
+
const frontmatterMatch = skillMdContent.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
773
|
+
if (frontmatterMatch) {
|
|
774
|
+
const lines = frontmatterMatch[1].split("\n");
|
|
775
|
+
for (const line of lines) {
|
|
776
|
+
const [key, ...valueParts] = line.split(":");
|
|
777
|
+
const value = valueParts.join(":").trim();
|
|
778
|
+
if (key.trim() === "name") name = value;
|
|
779
|
+
if (key.trim() === "description") description = value;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
} catch {
|
|
783
|
+
skillMdContent = `---
|
|
784
|
+
name: ${id}
|
|
785
|
+
description: Custom skill
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
# ${id}
|
|
789
|
+
|
|
790
|
+
Describe your skill here.
|
|
791
|
+
`;
|
|
792
|
+
}
|
|
793
|
+
const listFiles = async (subfolder) => {
|
|
794
|
+
try {
|
|
795
|
+
const files = await fs.promises.readdir(path.join(skillPath, subfolder));
|
|
796
|
+
return files;
|
|
797
|
+
} catch {
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
const scripts = await listFiles("scripts");
|
|
802
|
+
const references = await listFiles("references");
|
|
803
|
+
const assets = await listFiles("assets");
|
|
804
|
+
const stats = await fs.promises.stat(skillPath);
|
|
805
|
+
return {
|
|
806
|
+
success: true,
|
|
807
|
+
item: {
|
|
808
|
+
id,
|
|
809
|
+
name,
|
|
810
|
+
folderName: id,
|
|
811
|
+
icon: "/Sk",
|
|
812
|
+
description,
|
|
813
|
+
color: "#ff5252",
|
|
814
|
+
createdAt: stats.birthtime.toISOString(),
|
|
815
|
+
updatedAt: stats.mtime.toISOString(),
|
|
816
|
+
skillMdContent,
|
|
817
|
+
scripts,
|
|
818
|
+
references,
|
|
819
|
+
assets
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
} catch (error) {
|
|
823
|
+
console.error("Error getting skill:", error);
|
|
824
|
+
throw new Error(error.message || "Failed to get skill");
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
electron.ipcMain.handle(
|
|
828
|
+
"nodeLibrary:skill:create",
|
|
829
|
+
async (_, data) => {
|
|
830
|
+
try {
|
|
831
|
+
await ensureLibraryExists();
|
|
832
|
+
const folderName = data.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
833
|
+
const skillPath = path.join(skillsFolder(), folderName);
|
|
834
|
+
try {
|
|
835
|
+
await fs.promises.access(skillPath);
|
|
836
|
+
throw new Error(`Skill "${data.name}" already exists`);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
if (err.code !== "ENOENT") throw err;
|
|
839
|
+
}
|
|
840
|
+
await fs.promises.mkdir(skillPath, { recursive: true });
|
|
841
|
+
await fs.promises.mkdir(path.join(skillPath, "scripts"), { recursive: true });
|
|
842
|
+
await fs.promises.mkdir(path.join(skillPath, "references"), { recursive: true });
|
|
843
|
+
await fs.promises.mkdir(path.join(skillPath, "assets"), { recursive: true });
|
|
844
|
+
const skillMdContent = `---
|
|
845
|
+
name: ${data.name}
|
|
846
|
+
description: ${data.description || "Custom skill"}
|
|
847
|
+
---
|
|
848
|
+
|
|
849
|
+
# ${data.name}
|
|
850
|
+
|
|
851
|
+
${data.description || "Describe your skill here."}
|
|
852
|
+
|
|
853
|
+
## Instructions
|
|
854
|
+
|
|
855
|
+
1. Step one
|
|
856
|
+
2. Step two
|
|
857
|
+
3. Step three
|
|
858
|
+
`;
|
|
859
|
+
await fs.promises.writeFile(path.join(skillPath, "SKILL.md"), skillMdContent, "utf-8");
|
|
860
|
+
const stats = await fs.promises.stat(skillPath);
|
|
861
|
+
return {
|
|
862
|
+
success: true,
|
|
863
|
+
item: {
|
|
864
|
+
id: folderName,
|
|
865
|
+
name: data.name,
|
|
866
|
+
folderName,
|
|
867
|
+
icon: "/Sk",
|
|
868
|
+
description: data.description || "Custom skill",
|
|
869
|
+
color: "#ff5252",
|
|
870
|
+
createdAt: stats.birthtime.toISOString(),
|
|
871
|
+
updatedAt: stats.mtime.toISOString()
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
} catch (error) {
|
|
875
|
+
console.error("Error creating skill:", error);
|
|
876
|
+
throw new Error(error.message || "Failed to create skill");
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
);
|
|
880
|
+
electron.ipcMain.handle(
|
|
881
|
+
"nodeLibrary:skill:update",
|
|
882
|
+
async (_, id, updates) => {
|
|
883
|
+
try {
|
|
884
|
+
const skillPath = path.join(skillsFolder(), id);
|
|
885
|
+
const skillMdPath = path.join(skillPath, "SKILL.md");
|
|
886
|
+
await fs.promises.access(skillPath);
|
|
887
|
+
if (updates.skillMdContent !== void 0) {
|
|
888
|
+
await fs.promises.writeFile(skillMdPath, updates.skillMdContent, "utf-8");
|
|
889
|
+
} else if (updates.name !== void 0 || updates.description !== void 0) {
|
|
890
|
+
let currentContent = "";
|
|
891
|
+
try {
|
|
892
|
+
currentContent = await fs.promises.readFile(skillMdPath, "utf-8");
|
|
893
|
+
} catch {
|
|
894
|
+
currentContent = `---
|
|
895
|
+
name: ${id}
|
|
896
|
+
description: Custom skill
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
# ${id}
|
|
900
|
+
|
|
901
|
+
Describe your skill here.
|
|
902
|
+
`;
|
|
903
|
+
}
|
|
904
|
+
const frontmatterMatch = currentContent.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
905
|
+
let frontmatter = {};
|
|
906
|
+
let bodyContent = currentContent;
|
|
907
|
+
if (frontmatterMatch) {
|
|
908
|
+
const lines = frontmatterMatch[1].split("\n");
|
|
909
|
+
for (const line of lines) {
|
|
910
|
+
const colonIndex = line.indexOf(":");
|
|
911
|
+
if (colonIndex > 0) {
|
|
912
|
+
const key = line.substring(0, colonIndex).trim();
|
|
913
|
+
const value = line.substring(colonIndex + 1).trim();
|
|
914
|
+
frontmatter[key] = value;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
bodyContent = currentContent.substring(frontmatterMatch[0].length).trim();
|
|
918
|
+
}
|
|
919
|
+
if (updates.name !== void 0) {
|
|
920
|
+
frontmatter["name"] = updates.name;
|
|
921
|
+
}
|
|
922
|
+
if (updates.description !== void 0) {
|
|
923
|
+
frontmatter["description"] = updates.description;
|
|
924
|
+
}
|
|
925
|
+
const frontmatterLines = Object.entries(frontmatter).map(([key, value]) => `${key}: ${value}`).join("\n");
|
|
926
|
+
const newContent = `---
|
|
927
|
+
${frontmatterLines}
|
|
928
|
+
---
|
|
929
|
+
|
|
930
|
+
${bodyContent}`;
|
|
931
|
+
await fs.promises.writeFile(skillMdPath, newContent, "utf-8");
|
|
932
|
+
}
|
|
933
|
+
return { success: true };
|
|
934
|
+
} catch (error) {
|
|
935
|
+
console.error("Error updating skill:", error);
|
|
936
|
+
throw new Error(error.message || "Failed to update skill");
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
);
|
|
940
|
+
electron.ipcMain.handle("nodeLibrary:skill:delete", async (_, id) => {
|
|
941
|
+
try {
|
|
942
|
+
const skillPath = path.join(skillsFolder(), id);
|
|
943
|
+
await fs.promises.rm(skillPath, { recursive: true, force: true });
|
|
944
|
+
return { success: true };
|
|
945
|
+
} catch (error) {
|
|
946
|
+
console.error("Error deleting skill:", error);
|
|
947
|
+
throw new Error(error.message || "Failed to delete skill");
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
electron.ipcMain.handle(
|
|
951
|
+
"nodeLibrary:skill:addFile",
|
|
952
|
+
async (_, id, fileType, sourcePath, fileName) => {
|
|
953
|
+
try {
|
|
954
|
+
const skillPath = path.join(skillsFolder(), id);
|
|
955
|
+
const subfolderMap = {
|
|
956
|
+
script: "scripts",
|
|
957
|
+
reference: "references",
|
|
958
|
+
asset: "assets"
|
|
959
|
+
};
|
|
960
|
+
const destFolder = path.join(skillPath, subfolderMap[fileType]);
|
|
961
|
+
const destPath = path.join(destFolder, fileName);
|
|
962
|
+
await fs.promises.mkdir(destFolder, { recursive: true });
|
|
963
|
+
await fs.promises.copyFile(sourcePath, destPath);
|
|
964
|
+
return { success: true };
|
|
965
|
+
} catch (error) {
|
|
966
|
+
console.error("Error adding skill file:", error);
|
|
967
|
+
throw new Error(error.message || "Failed to add file");
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
);
|
|
971
|
+
electron.ipcMain.handle(
|
|
972
|
+
"nodeLibrary:skill:deleteFile",
|
|
973
|
+
async (_, id, fileType, fileName) => {
|
|
974
|
+
try {
|
|
975
|
+
const skillPath = path.join(skillsFolder(), id);
|
|
976
|
+
const subfolderMap = {
|
|
977
|
+
script: "scripts",
|
|
978
|
+
reference: "references",
|
|
979
|
+
asset: "assets"
|
|
980
|
+
};
|
|
981
|
+
const filePath = path.join(skillPath, subfolderMap[fileType], fileName);
|
|
982
|
+
await fs.promises.unlink(filePath);
|
|
983
|
+
return { success: true };
|
|
984
|
+
} catch (error) {
|
|
985
|
+
console.error("Error deleting skill file:", error);
|
|
986
|
+
throw new Error(error.message || "Failed to delete file");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
);
|
|
990
|
+
electron.ipcMain.handle(
|
|
991
|
+
"nodeLibrary:skill:copyToProject",
|
|
992
|
+
async (_, skillId, projectPath, forceSync, targetFolderName) => {
|
|
993
|
+
try {
|
|
994
|
+
const librarySkillPath = path.join(skillsFolder(), skillId);
|
|
995
|
+
const destFolderName = targetFolderName || skillId;
|
|
996
|
+
const projectSkillPath = path.join(projectPath, ".claude", "skills", destFolderName);
|
|
997
|
+
try {
|
|
998
|
+
await fs.promises.access(librarySkillPath);
|
|
999
|
+
} catch {
|
|
1000
|
+
return { success: false, error: "Library skill not found" };
|
|
1001
|
+
}
|
|
1002
|
+
let alreadyExists = false;
|
|
1003
|
+
try {
|
|
1004
|
+
await fs.promises.access(projectSkillPath);
|
|
1005
|
+
alreadyExists = true;
|
|
1006
|
+
if (!forceSync) {
|
|
1007
|
+
return { success: true, alreadyExists: true, folderName: destFolderName };
|
|
1008
|
+
}
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
await fs.promises.mkdir(path.join(projectPath, ".claude", "skills"), { recursive: true });
|
|
1012
|
+
const copyDir = async (src, dest) => {
|
|
1013
|
+
await fs.promises.mkdir(dest, { recursive: true });
|
|
1014
|
+
const entries = await fs.promises.readdir(src, { withFileTypes: true });
|
|
1015
|
+
for (const entry of entries) {
|
|
1016
|
+
const srcPath = path.join(src, entry.name);
|
|
1017
|
+
const destPath = path.join(dest, entry.name);
|
|
1018
|
+
if (entry.isDirectory()) {
|
|
1019
|
+
await copyDir(srcPath, destPath);
|
|
1020
|
+
} else {
|
|
1021
|
+
try {
|
|
1022
|
+
await fs.promises.access(destPath);
|
|
1023
|
+
continue;
|
|
1024
|
+
} catch {
|
|
1025
|
+
await fs.promises.copyFile(srcPath, destPath);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
await copyDir(librarySkillPath, projectSkillPath);
|
|
1031
|
+
return { success: true, alreadyExists, synced: forceSync, folderName: destFolderName };
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
console.error("Error copying skill to project:", error);
|
|
1034
|
+
throw new Error(error.message || "Failed to copy skill to project");
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
);
|
|
1038
|
+
};
|
|
1039
|
+
const registerMemoryHandlers = () => {
|
|
1040
|
+
const memoryFolder = () => path.join(getLibraryPath(), "memory");
|
|
1041
|
+
electron.ipcMain.handle("nodeLibrary:memory:list", async () => {
|
|
1042
|
+
try {
|
|
1043
|
+
await ensureLibraryExists();
|
|
1044
|
+
const memoryPath = memoryFolder();
|
|
1045
|
+
let entries = [];
|
|
1046
|
+
try {
|
|
1047
|
+
entries = await fs.promises.readdir(memoryPath, { withFileTypes: true });
|
|
1048
|
+
} catch {
|
|
1049
|
+
return { success: true, items: [] };
|
|
1050
|
+
}
|
|
1051
|
+
const memories = [];
|
|
1052
|
+
for (const entry of entries) {
|
|
1053
|
+
if (entry.isDirectory()) {
|
|
1054
|
+
const nodePath = path.join(memoryPath, entry.name);
|
|
1055
|
+
const metadataPath = path.join(nodePath, "_metadata.json");
|
|
1056
|
+
let name = entry.name;
|
|
1057
|
+
let description = "";
|
|
1058
|
+
let icon2 = "/Me";
|
|
1059
|
+
let iconImage = "";
|
|
1060
|
+
try {
|
|
1061
|
+
const content = await fs.promises.readFile(metadataPath, "utf-8");
|
|
1062
|
+
const metadata = JSON.parse(content);
|
|
1063
|
+
if (metadata._node) {
|
|
1064
|
+
name = metadata._node.name || entry.name;
|
|
1065
|
+
description = metadata._node.description || "";
|
|
1066
|
+
icon2 = metadata._node.icon || "/Me";
|
|
1067
|
+
iconImage = metadata._node.iconImage || "";
|
|
1068
|
+
}
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
const files = await fs.promises.readdir(nodePath);
|
|
1072
|
+
const fileCount = files.filter((f) => f !== "_metadata.json").length;
|
|
1073
|
+
const stats = await fs.promises.stat(nodePath);
|
|
1074
|
+
memories.push({
|
|
1075
|
+
id: entry.name,
|
|
1076
|
+
name,
|
|
1077
|
+
folderName: entry.name,
|
|
1078
|
+
icon: icon2,
|
|
1079
|
+
iconImage,
|
|
1080
|
+
description: description || `${fileCount} file(s)`,
|
|
1081
|
+
color: "#4ade80",
|
|
1082
|
+
createdAt: stats.birthtime.toISOString(),
|
|
1083
|
+
updatedAt: stats.mtime.toISOString()
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return { success: true, items: memories };
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
console.error("Error listing memory nodes:", error);
|
|
1090
|
+
throw new Error(error.message || "Failed to list memory nodes");
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
electron.ipcMain.handle("nodeLibrary:memory:get", async (_, id) => {
|
|
1094
|
+
try {
|
|
1095
|
+
const nodePath = path.join(memoryFolder(), id);
|
|
1096
|
+
const metadataPath = path.join(nodePath, "_metadata.json");
|
|
1097
|
+
await fs.promises.access(nodePath);
|
|
1098
|
+
let name = id;
|
|
1099
|
+
let description = "";
|
|
1100
|
+
let icon2 = "/Me";
|
|
1101
|
+
let iconImage = "";
|
|
1102
|
+
let fileMetadata = {};
|
|
1103
|
+
try {
|
|
1104
|
+
const content = await fs.promises.readFile(metadataPath, "utf-8");
|
|
1105
|
+
const metadata = JSON.parse(content);
|
|
1106
|
+
if (metadata._node) {
|
|
1107
|
+
name = metadata._node.name || id;
|
|
1108
|
+
description = metadata._node.description || "";
|
|
1109
|
+
icon2 = metadata._node.icon || "/Me";
|
|
1110
|
+
iconImage = metadata._node.iconImage || "";
|
|
1111
|
+
}
|
|
1112
|
+
const { _node, ...rest } = metadata;
|
|
1113
|
+
fileMetadata = rest;
|
|
1114
|
+
} catch {
|
|
1115
|
+
}
|
|
1116
|
+
const allFiles = await fs.promises.readdir(nodePath);
|
|
1117
|
+
const files = allFiles.filter((f) => f !== "_metadata.json");
|
|
1118
|
+
const fileInfos = await Promise.all(
|
|
1119
|
+
files.map(async (fileName) => {
|
|
1120
|
+
const filePath = path.join(nodePath, fileName);
|
|
1121
|
+
const stats2 = await fs.promises.stat(filePath);
|
|
1122
|
+
const meta = fileMetadata[fileName] || {};
|
|
1123
|
+
return {
|
|
1124
|
+
name: fileName,
|
|
1125
|
+
title: meta.title || fileName,
|
|
1126
|
+
description: meta.description || "",
|
|
1127
|
+
size: stats2.size,
|
|
1128
|
+
created: meta.created || stats2.birthtime.toISOString()
|
|
1129
|
+
};
|
|
1130
|
+
})
|
|
1131
|
+
);
|
|
1132
|
+
const stats = await fs.promises.stat(nodePath);
|
|
1133
|
+
return {
|
|
1134
|
+
success: true,
|
|
1135
|
+
item: {
|
|
1136
|
+
id,
|
|
1137
|
+
name,
|
|
1138
|
+
folderName: id,
|
|
1139
|
+
icon: icon2,
|
|
1140
|
+
iconImage,
|
|
1141
|
+
description,
|
|
1142
|
+
color: "#4ade80",
|
|
1143
|
+
createdAt: stats.birthtime.toISOString(),
|
|
1144
|
+
updatedAt: stats.mtime.toISOString(),
|
|
1145
|
+
files: fileInfos
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
console.error("Error getting memory node:", error);
|
|
1150
|
+
throw new Error(error.message || "Failed to get memory node");
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
electron.ipcMain.handle(
|
|
1154
|
+
"nodeLibrary:memory:create",
|
|
1155
|
+
async (_, data) => {
|
|
1156
|
+
try {
|
|
1157
|
+
await ensureLibraryExists();
|
|
1158
|
+
const folderName = data.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
1159
|
+
const nodePath = path.join(memoryFolder(), folderName);
|
|
1160
|
+
try {
|
|
1161
|
+
await fs.promises.access(nodePath);
|
|
1162
|
+
throw new Error(`Memory node "${data.name}" already exists`);
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
if (err.code !== "ENOENT") throw err;
|
|
1165
|
+
}
|
|
1166
|
+
await fs.promises.mkdir(nodePath, { recursive: true });
|
|
1167
|
+
const metadata = {
|
|
1168
|
+
_node: {
|
|
1169
|
+
name: data.name,
|
|
1170
|
+
description: data.description || "",
|
|
1171
|
+
icon: data.icon || "/Me",
|
|
1172
|
+
iconImage: data.iconImage || ""
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
await fs.promises.writeFile(path.join(nodePath, "_metadata.json"), JSON.stringify(metadata, null, 2), "utf-8");
|
|
1176
|
+
const stats = await fs.promises.stat(nodePath);
|
|
1177
|
+
return {
|
|
1178
|
+
success: true,
|
|
1179
|
+
item: {
|
|
1180
|
+
id: folderName,
|
|
1181
|
+
name: data.name,
|
|
1182
|
+
folderName,
|
|
1183
|
+
icon: data.icon || "/Me",
|
|
1184
|
+
iconImage: data.iconImage || "",
|
|
1185
|
+
description: data.description || "",
|
|
1186
|
+
color: "#4ade80",
|
|
1187
|
+
createdAt: stats.birthtime.toISOString(),
|
|
1188
|
+
updatedAt: stats.mtime.toISOString()
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
console.error("Error creating memory node:", error);
|
|
1193
|
+
throw new Error(error.message || "Failed to create memory node");
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
);
|
|
1197
|
+
electron.ipcMain.handle(
|
|
1198
|
+
"nodeLibrary:memory:update",
|
|
1199
|
+
async (_, id, updates) => {
|
|
1200
|
+
try {
|
|
1201
|
+
const nodePath = path.join(memoryFolder(), id);
|
|
1202
|
+
const metadataPath = path.join(nodePath, "_metadata.json");
|
|
1203
|
+
await fs.promises.access(nodePath);
|
|
1204
|
+
let metadata = {};
|
|
1205
|
+
try {
|
|
1206
|
+
const content = await fs.promises.readFile(metadataPath, "utf-8");
|
|
1207
|
+
metadata = JSON.parse(content);
|
|
1208
|
+
} catch {
|
|
1209
|
+
metadata = {};
|
|
1210
|
+
}
|
|
1211
|
+
metadata._node = {
|
|
1212
|
+
...metadata._node || {},
|
|
1213
|
+
...updates
|
|
1214
|
+
};
|
|
1215
|
+
await fs.promises.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1216
|
+
return { success: true };
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
console.error("Error updating memory node:", error);
|
|
1219
|
+
throw new Error(error.message || "Failed to update memory node");
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
);
|
|
1223
|
+
electron.ipcMain.handle("nodeLibrary:memory:delete", async (_, id) => {
|
|
1224
|
+
try {
|
|
1225
|
+
const nodePath = path.join(memoryFolder(), id);
|
|
1226
|
+
await fs.promises.rm(nodePath, { recursive: true, force: true });
|
|
1227
|
+
return { success: true };
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
console.error("Error deleting memory node:", error);
|
|
1230
|
+
throw new Error(error.message || "Failed to delete memory node");
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
electron.ipcMain.handle(
|
|
1234
|
+
"nodeLibrary:memory:addFile",
|
|
1235
|
+
async (_, id, sourcePath, fileName, title, description) => {
|
|
1236
|
+
try {
|
|
1237
|
+
const nodePath = path.join(memoryFolder(), id);
|
|
1238
|
+
const destPath = path.join(nodePath, fileName);
|
|
1239
|
+
const metadataPath = path.join(nodePath, "_metadata.json");
|
|
1240
|
+
await fs.promises.copyFile(sourcePath, destPath);
|
|
1241
|
+
let metadata = {};
|
|
1242
|
+
try {
|
|
1243
|
+
const content = await fs.promises.readFile(metadataPath, "utf-8");
|
|
1244
|
+
metadata = JSON.parse(content);
|
|
1245
|
+
} catch {
|
|
1246
|
+
metadata = { _node: { name: id, description: "" } };
|
|
1247
|
+
}
|
|
1248
|
+
metadata[fileName] = {
|
|
1249
|
+
title: title || fileName,
|
|
1250
|
+
description: description || "",
|
|
1251
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
1252
|
+
};
|
|
1253
|
+
await fs.promises.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1254
|
+
return { success: true };
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
console.error("Error adding memory file:", error);
|
|
1257
|
+
throw new Error(error.message || "Failed to add file");
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
);
|
|
1261
|
+
electron.ipcMain.handle("nodeLibrary:memory:deleteFile", async (_, id, fileName) => {
|
|
1262
|
+
try {
|
|
1263
|
+
const nodePath = path.join(memoryFolder(), id);
|
|
1264
|
+
const filePath = path.join(nodePath, fileName);
|
|
1265
|
+
const metadataPath = path.join(nodePath, "_metadata.json");
|
|
1266
|
+
await fs.promises.unlink(filePath);
|
|
1267
|
+
try {
|
|
1268
|
+
const content = await fs.promises.readFile(metadataPath, "utf-8");
|
|
1269
|
+
const metadata = JSON.parse(content);
|
|
1270
|
+
delete metadata[fileName];
|
|
1271
|
+
await fs.promises.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
1272
|
+
} catch {
|
|
1273
|
+
}
|
|
1274
|
+
return { success: true };
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
console.error("Error deleting memory file:", error);
|
|
1277
|
+
throw new Error(error.message || "Failed to delete file");
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
electron.ipcMain.handle(
|
|
1281
|
+
"nodeLibrary:memory:copyToProject",
|
|
1282
|
+
async (_, memoryId, projectPath, forceSync, targetFolderName) => {
|
|
1283
|
+
try {
|
|
1284
|
+
const libraryMemoryPath = path.join(memoryFolder(), memoryId);
|
|
1285
|
+
const destFolderName = targetFolderName || memoryId;
|
|
1286
|
+
const projectMemoryPath = path.join(projectPath, ".claude", "memory", destFolderName);
|
|
1287
|
+
try {
|
|
1288
|
+
await fs.promises.access(libraryMemoryPath);
|
|
1289
|
+
} catch {
|
|
1290
|
+
return { success: false, error: "Library memory not found" };
|
|
1291
|
+
}
|
|
1292
|
+
let alreadyExists = false;
|
|
1293
|
+
try {
|
|
1294
|
+
await fs.promises.access(projectMemoryPath);
|
|
1295
|
+
alreadyExists = true;
|
|
1296
|
+
if (!forceSync) {
|
|
1297
|
+
return { success: true, alreadyExists: true, folderName: destFolderName };
|
|
1298
|
+
}
|
|
1299
|
+
} catch {
|
|
1300
|
+
}
|
|
1301
|
+
await fs.promises.mkdir(path.join(projectPath, ".claude", "memory"), { recursive: true });
|
|
1302
|
+
const copyDir = async (src, dest) => {
|
|
1303
|
+
await fs.promises.mkdir(dest, { recursive: true });
|
|
1304
|
+
const entries = await fs.promises.readdir(src, { withFileTypes: true });
|
|
1305
|
+
for (const entry of entries) {
|
|
1306
|
+
const srcPath = path.join(src, entry.name);
|
|
1307
|
+
const destPath = path.join(dest, entry.name);
|
|
1308
|
+
if (entry.isDirectory()) {
|
|
1309
|
+
await copyDir(srcPath, destPath);
|
|
1310
|
+
} else {
|
|
1311
|
+
try {
|
|
1312
|
+
await fs.promises.access(destPath);
|
|
1313
|
+
} catch {
|
|
1314
|
+
await fs.promises.copyFile(srcPath, destPath);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
await copyDir(libraryMemoryPath, projectMemoryPath);
|
|
1320
|
+
return { success: true, alreadyExists, synced: forceSync, folderName: destFolderName };
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
console.error("Error copying memory to project:", error);
|
|
1323
|
+
throw new Error(error.message || "Failed to copy memory to project");
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
);
|
|
1327
|
+
};
|
|
1328
|
+
const registerMigrationHandler = () => {
|
|
1329
|
+
electron.ipcMain.handle("nodeLibrary:migrate:fromNodeData", async () => {
|
|
1330
|
+
try {
|
|
1331
|
+
await ensureLibraryExists();
|
|
1332
|
+
const defaultMCPs = [
|
|
1333
|
+
{
|
|
1334
|
+
id: "mcp-example",
|
|
1335
|
+
name: "Example MCP",
|
|
1336
|
+
icon: "E",
|
|
1337
|
+
description: "Example MCP Server integration",
|
|
1338
|
+
color: "#60a5fa",
|
|
1339
|
+
command: "npx",
|
|
1340
|
+
args: ["-y", "example-mcp", "--api-key", "YOUR_API_KEY"],
|
|
1341
|
+
env: {},
|
|
1342
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1343
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1344
|
+
}
|
|
1345
|
+
];
|
|
1346
|
+
const defaultHooks = [
|
|
1347
|
+
{
|
|
1348
|
+
id: "hook-pretooluse",
|
|
1349
|
+
name: "Pre Tool Use",
|
|
1350
|
+
icon: "⚡",
|
|
1351
|
+
description: "Execute before tool call is processed",
|
|
1352
|
+
color: "#fcd34d",
|
|
1353
|
+
eventType: "PreToolUse",
|
|
1354
|
+
script: '#!/bin/bash\n# Pre-tool use hook\necho "Tool: $TOOL_NAME"',
|
|
1355
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1356
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
id: "hook-posttooluse",
|
|
1360
|
+
name: "Post Tool Use",
|
|
1361
|
+
icon: "✓",
|
|
1362
|
+
description: "Execute after tool completes successfully",
|
|
1363
|
+
color: "#fcd34d",
|
|
1364
|
+
eventType: "PostToolUse",
|
|
1365
|
+
script: '#!/bin/bash\n# Post-tool use hook\necho "Tool completed: $TOOL_NAME"',
|
|
1366
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1367
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1368
|
+
},
|
|
1369
|
+
{
|
|
1370
|
+
id: "hook-userpromptsubmit",
|
|
1371
|
+
name: "User Prompt Submit",
|
|
1372
|
+
icon: "💬",
|
|
1373
|
+
description: "Execute when user submits a prompt",
|
|
1374
|
+
color: "#fcd34d",
|
|
1375
|
+
eventType: "UserPromptSubmit",
|
|
1376
|
+
script: "#!/bin/bash\n# User prompt submit hook",
|
|
1377
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1378
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
id: "hook-notification",
|
|
1382
|
+
name: "Notification",
|
|
1383
|
+
icon: "🔔",
|
|
1384
|
+
description: "Execute when notification is sent",
|
|
1385
|
+
color: "#fcd34d",
|
|
1386
|
+
eventType: "Notification",
|
|
1387
|
+
script: "#!/bin/bash\n# Notification hook",
|
|
1388
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1389
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
id: "hook-stop",
|
|
1393
|
+
name: "Stop",
|
|
1394
|
+
icon: "⏹",
|
|
1395
|
+
description: "Execute when agent finishes responding",
|
|
1396
|
+
color: "#fcd34d",
|
|
1397
|
+
eventType: "Stop",
|
|
1398
|
+
script: "#!/bin/bash\n# Stop hook",
|
|
1399
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1400
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1401
|
+
}
|
|
1402
|
+
];
|
|
1403
|
+
const defaultSubAgents = [
|
|
1404
|
+
{
|
|
1405
|
+
id: "subagent-manager",
|
|
1406
|
+
name: "SubAgent Manager",
|
|
1407
|
+
icon: "🤖",
|
|
1408
|
+
description: "Manage specialized subagents for your workflow",
|
|
1409
|
+
color: "#8b5cf6",
|
|
1410
|
+
systemPrompt: "You are a SubAgent Manager. Your role is to coordinate and delegate tasks to specialized subagents to achieve complex goals efficiently.",
|
|
1411
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1412
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1413
|
+
}
|
|
1414
|
+
];
|
|
1415
|
+
const defaultCommands = [
|
|
1416
|
+
{
|
|
1417
|
+
id: "default-command",
|
|
1418
|
+
name: "Custom Command",
|
|
1419
|
+
icon: "C",
|
|
1420
|
+
description: "Define and manage custom commands for your workflow",
|
|
1421
|
+
color: "hsl(294, 88%, 87%)",
|
|
1422
|
+
promptTemplate: "# Custom Command\n\nDescribe your command here.",
|
|
1423
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1424
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1425
|
+
}
|
|
1426
|
+
];
|
|
1427
|
+
const libraryPath = getLibraryPath();
|
|
1428
|
+
const migrateIfNeeded = async (fileName, defaultItems) => {
|
|
1429
|
+
const filePath = path.join(libraryPath, fileName);
|
|
1430
|
+
try {
|
|
1431
|
+
await fs.promises.access(filePath);
|
|
1432
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
1433
|
+
const data = JSON.parse(content);
|
|
1434
|
+
if (data.items && data.items.length > 0) {
|
|
1435
|
+
return { migrated: false, count: data.items.length };
|
|
1436
|
+
}
|
|
1437
|
+
} catch {
|
|
1438
|
+
}
|
|
1439
|
+
await saveJsonFile(fileName, { version: "1.0.0", items: defaultItems });
|
|
1440
|
+
return { migrated: true, count: defaultItems.length };
|
|
1441
|
+
};
|
|
1442
|
+
const results = {
|
|
1443
|
+
mcps: await migrateIfNeeded("mcps.json", defaultMCPs),
|
|
1444
|
+
hooks: await migrateIfNeeded("hooks.json", defaultHooks),
|
|
1445
|
+
subagents: await migrateIfNeeded("subagents.json", defaultSubAgents),
|
|
1446
|
+
commands: await migrateIfNeeded("commands.json", defaultCommands),
|
|
1447
|
+
skills: { migrated: false, count: 0 }
|
|
1448
|
+
};
|
|
1449
|
+
const skillsPath = path.join(libraryPath, "skills");
|
|
1450
|
+
const defaultSkills = [
|
|
1451
|
+
{
|
|
1452
|
+
folderName: "code-review",
|
|
1453
|
+
name: "Code Review",
|
|
1454
|
+
description: "Perform thorough code reviews with best practices",
|
|
1455
|
+
content: `---
|
|
1456
|
+
name: Code Review
|
|
1457
|
+
description: Perform thorough code reviews with best practices
|
|
1458
|
+
---
|
|
1459
|
+
|
|
1460
|
+
# Code Review Skill
|
|
1461
|
+
|
|
1462
|
+
## Overview
|
|
1463
|
+
This skill helps you perform thorough code reviews focusing on code quality, security, and best practices.
|
|
1464
|
+
|
|
1465
|
+
## Instructions
|
|
1466
|
+
|
|
1467
|
+
1. **Analyze the code structure** - Check for proper organization and modularity
|
|
1468
|
+
2. **Review naming conventions** - Ensure variables, functions, and classes have descriptive names
|
|
1469
|
+
3. **Check for security issues** - Look for common vulnerabilities (injection, XSS, etc.)
|
|
1470
|
+
4. **Evaluate performance** - Identify potential bottlenecks or inefficient code
|
|
1471
|
+
5. **Verify error handling** - Ensure proper error handling and edge cases
|
|
1472
|
+
6. **Review documentation** - Check for adequate comments and documentation
|
|
1473
|
+
|
|
1474
|
+
## Output Format
|
|
1475
|
+
Provide feedback in a structured format with:
|
|
1476
|
+
- Summary of findings
|
|
1477
|
+
- Critical issues (if any)
|
|
1478
|
+
- Suggestions for improvement
|
|
1479
|
+
- Positive observations
|
|
1480
|
+
`
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
folderName: "documentation-writer",
|
|
1484
|
+
name: "Documentation Writer",
|
|
1485
|
+
description: "Generate comprehensive documentation for code and APIs",
|
|
1486
|
+
content: `---
|
|
1487
|
+
name: Documentation Writer
|
|
1488
|
+
description: Generate comprehensive documentation for code and APIs
|
|
1489
|
+
---
|
|
1490
|
+
|
|
1491
|
+
# Documentation Writer Skill
|
|
1492
|
+
|
|
1493
|
+
## Overview
|
|
1494
|
+
This skill helps you create clear, comprehensive documentation for code, APIs, and projects.
|
|
1495
|
+
|
|
1496
|
+
## Instructions
|
|
1497
|
+
|
|
1498
|
+
1. **Understand the context** - Review the code or API to document
|
|
1499
|
+
2. **Identify the audience** - Determine who will read the documentation
|
|
1500
|
+
3. **Structure the content** - Organize with clear headings and sections
|
|
1501
|
+
4. **Write clear explanations** - Use simple language and examples
|
|
1502
|
+
5. **Include code samples** - Provide working examples where applicable
|
|
1503
|
+
6. **Add troubleshooting tips** - Anticipate common issues
|
|
1504
|
+
|
|
1505
|
+
## Documentation Types
|
|
1506
|
+
- README files
|
|
1507
|
+
- API documentation
|
|
1508
|
+
- Code comments
|
|
1509
|
+
- User guides
|
|
1510
|
+
- Technical specifications
|
|
1511
|
+
`
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
folderName: "test-generator",
|
|
1515
|
+
name: "Test Generator",
|
|
1516
|
+
description: "Generate unit tests and integration tests for your code",
|
|
1517
|
+
content: `---
|
|
1518
|
+
name: Test Generator
|
|
1519
|
+
description: Generate unit tests and integration tests for your code
|
|
1520
|
+
---
|
|
1521
|
+
|
|
1522
|
+
# Test Generator Skill
|
|
1523
|
+
|
|
1524
|
+
## Overview
|
|
1525
|
+
This skill helps you generate comprehensive test suites including unit tests, integration tests, and edge cases.
|
|
1526
|
+
|
|
1527
|
+
## Instructions
|
|
1528
|
+
|
|
1529
|
+
1. **Analyze the code** - Understand what needs to be tested
|
|
1530
|
+
2. **Identify test cases** - List all scenarios to cover
|
|
1531
|
+
3. **Write unit tests** - Test individual functions and methods
|
|
1532
|
+
4. **Add edge cases** - Test boundary conditions and error scenarios
|
|
1533
|
+
5. **Include mocks** - Mock external dependencies when needed
|
|
1534
|
+
6. **Ensure coverage** - Aim for high code coverage
|
|
1535
|
+
|
|
1536
|
+
## Test Patterns
|
|
1537
|
+
- Arrange-Act-Assert (AAA)
|
|
1538
|
+
- Given-When-Then (BDD)
|
|
1539
|
+
- Test doubles (mocks, stubs, spies)
|
|
1540
|
+
|
|
1541
|
+
## Frameworks Support
|
|
1542
|
+
- Jest (JavaScript/TypeScript)
|
|
1543
|
+
- Pytest (Python)
|
|
1544
|
+
- JUnit (Java)
|
|
1545
|
+
- And more...
|
|
1546
|
+
`
|
|
1547
|
+
}
|
|
1548
|
+
];
|
|
1549
|
+
for (const skill of defaultSkills) {
|
|
1550
|
+
const skillPath = path.join(skillsPath, skill.folderName);
|
|
1551
|
+
try {
|
|
1552
|
+
await fs.promises.access(skillPath);
|
|
1553
|
+
} catch {
|
|
1554
|
+
await fs.promises.mkdir(skillPath, { recursive: true });
|
|
1555
|
+
await fs.promises.mkdir(path.join(skillPath, "scripts"), { recursive: true });
|
|
1556
|
+
await fs.promises.mkdir(path.join(skillPath, "references"), { recursive: true });
|
|
1557
|
+
await fs.promises.mkdir(path.join(skillPath, "assets"), { recursive: true });
|
|
1558
|
+
await fs.promises.writeFile(path.join(skillPath, "SKILL.md"), skill.content, "utf-8");
|
|
1559
|
+
results.skills.count++;
|
|
1560
|
+
results.skills.migrated = true;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return { success: true, results };
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
console.error("Error migrating node data:", error);
|
|
1566
|
+
throw new Error(error.message || "Failed to migrate node data");
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
};
|
|
1570
|
+
function registerNodeLibraryHandlers() {
|
|
1571
|
+
createJsonCrudHandlers("mcp", "mcps.json", "#60a5fa");
|
|
1572
|
+
createJsonCrudHandlers("hook", "hooks.json", "#fcd34d");
|
|
1573
|
+
createJsonCrudHandlers("hookconfig", "hookconfigs.json", "#fcd34d");
|
|
1574
|
+
createJsonCrudHandlers("subagent", "subagents.json", "#8b5cf6");
|
|
1575
|
+
createJsonCrudHandlers("command", "commands.json", "hsl(294, 88%, 87%)");
|
|
1576
|
+
registerSkillHandlers();
|
|
1577
|
+
registerMemoryHandlers();
|
|
1578
|
+
registerMigrationHandler();
|
|
1579
|
+
console.log("Node Library handlers registered");
|
|
1580
|
+
}
|
|
1581
|
+
let mainWindow = null;
|
|
1582
|
+
if (process.platform === "darwin" && electron.app.isPackaged) {
|
|
1583
|
+
try {
|
|
1584
|
+
const userShell = process.env.SHELL || "/bin/zsh";
|
|
1585
|
+
const shellPath = child_process.execSync(`${userShell} -ilc 'echo $PATH'`, {
|
|
1586
|
+
encoding: "utf8",
|
|
1587
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
1588
|
+
}).trim();
|
|
1589
|
+
if (shellPath) {
|
|
1590
|
+
process.env.PATH = shellPath;
|
|
1591
|
+
}
|
|
1592
|
+
} catch (error) {
|
|
1593
|
+
console.error("Failed to get shell PATH:", error);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const isDev = !electron.app.isPackaged;
|
|
1597
|
+
const SUPPORTED_AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".ogg"]);
|
|
1598
|
+
function getDefaultMusicDir() {
|
|
1599
|
+
if (isDev) return path.join(electron.app.getAppPath(), "resources", "music");
|
|
1600
|
+
return path.join(process.resourcesPath, "app.asar.unpacked", "resources", "music");
|
|
1601
|
+
}
|
|
1602
|
+
function getWorkspaceMusicDir(workspacePath) {
|
|
1603
|
+
return path.join(workspacePath, ".meta", "music");
|
|
1604
|
+
}
|
|
1605
|
+
function fileTitleFromPath(filePath) {
|
|
1606
|
+
const base = path.basename(filePath);
|
|
1607
|
+
const ext = path.extname(base);
|
|
1608
|
+
return ext ? base.slice(0, -ext.length) : base;
|
|
1609
|
+
}
|
|
1610
|
+
async function listAudioFilesInDir(dirPath) {
|
|
1611
|
+
try {
|
|
1612
|
+
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
1613
|
+
const files = entries.filter((e) => e.isFile()).map((e) => path.join(dirPath, e.name)).filter((p) => SUPPORTED_AUDIO_EXTS.has(path.extname(p).toLowerCase()));
|
|
1614
|
+
return files;
|
|
1615
|
+
} catch {
|
|
1616
|
+
return [];
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
function createWindow() {
|
|
1620
|
+
mainWindow = new electron.BrowserWindow({
|
|
1621
|
+
width: 1200,
|
|
1622
|
+
height: 800,
|
|
1623
|
+
minWidth: 1200,
|
|
1624
|
+
minHeight: 800,
|
|
1625
|
+
show: false,
|
|
1626
|
+
autoHideMenuBar: true,
|
|
1627
|
+
...process.platform === "linux" ? { icon } : {},
|
|
1628
|
+
webPreferences: {
|
|
1629
|
+
preload: path.join(__dirname, "../preload/index.js"),
|
|
1630
|
+
sandbox: false
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
mainWindow.on("ready-to-show", () => {
|
|
1634
|
+
mainWindow?.show();
|
|
1635
|
+
});
|
|
1636
|
+
if (isDev) {
|
|
1637
|
+
mainWindow.webContents.openDevTools();
|
|
1638
|
+
}
|
|
1639
|
+
mainWindow.webContents.setWindowOpenHandler((details) => {
|
|
1640
|
+
electron.shell.openExternal(details.url);
|
|
1641
|
+
return { action: "deny" };
|
|
1642
|
+
});
|
|
1643
|
+
if (isDev && process.env["ELECTRON_RENDERER_URL"]) {
|
|
1644
|
+
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
|
1645
|
+
} else {
|
|
1646
|
+
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function parseCliArguments(args) {
|
|
1650
|
+
let projectPath = null;
|
|
1651
|
+
let openCanvas = false;
|
|
1652
|
+
const projectPathIndex = args.indexOf("--project-path");
|
|
1653
|
+
if (projectPathIndex !== -1) {
|
|
1654
|
+
for (let i = projectPathIndex + 1; i < args.length; i++) {
|
|
1655
|
+
const arg = args[i];
|
|
1656
|
+
if (!arg.startsWith("--")) {
|
|
1657
|
+
projectPath = path.resolve(arg);
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
const openCanvasIndex = args.indexOf("--open-canvas");
|
|
1663
|
+
if (openCanvasIndex !== -1) {
|
|
1664
|
+
openCanvas = true;
|
|
1665
|
+
}
|
|
1666
|
+
if (projectPath && !fs.existsSync(projectPath)) {
|
|
1667
|
+
console.error(`Project path does not exist: ${projectPath}`);
|
|
1668
|
+
projectPath = null;
|
|
1669
|
+
}
|
|
1670
|
+
return { projectPath, openCanvas };
|
|
1671
|
+
}
|
|
1672
|
+
const gotTheLock = electron.app.requestSingleInstanceLock();
|
|
1673
|
+
if (!gotTheLock) {
|
|
1674
|
+
electron.app.quit();
|
|
1675
|
+
} else {
|
|
1676
|
+
electron.app.on("second-instance", async (_, commandLine) => {
|
|
1677
|
+
const { projectPath, openCanvas } = parseCliArguments(commandLine);
|
|
1678
|
+
if (mainWindow) {
|
|
1679
|
+
if (mainWindow.isMinimized()) mainWindow.restore();
|
|
1680
|
+
mainWindow.focus();
|
|
1681
|
+
if (mainWindow.webContents.isLoading()) {
|
|
1682
|
+
await new Promise((resolve2) => {
|
|
1683
|
+
mainWindow?.webContents.once("did-finish-load", resolve2);
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
if (projectPath) {
|
|
1687
|
+
if (openCanvas) {
|
|
1688
|
+
mainWindow.webContents.send("initial-open-canvas", true);
|
|
1689
|
+
}
|
|
1690
|
+
mainWindow.webContents.send("initial-project-path", projectPath);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
electron.app.whenReady().then(() => {
|
|
1696
|
+
utils.electronApp.setAppUserModelId("com.electron");
|
|
1697
|
+
electron.app.on("browser-window-created", (_, window) => {
|
|
1698
|
+
utils.optimizer.watchWindowShortcuts(window);
|
|
1699
|
+
});
|
|
1700
|
+
electron.ipcMain.on("ping", () => console.log("pong"));
|
|
1701
|
+
registerNodeLibraryHandlers();
|
|
1702
|
+
electron.ipcMain.handle("agent:start-session", async (_, projectPath2, apiKey, authMethod, sdkSessionId, permissionMode, agentFolderName) => {
|
|
1703
|
+
try {
|
|
1704
|
+
const sessionId = await agentManager.createSession({
|
|
1705
|
+
projectPath: projectPath2,
|
|
1706
|
+
apiKey,
|
|
1707
|
+
authMethod,
|
|
1708
|
+
sdkSessionId,
|
|
1709
|
+
permissionMode,
|
|
1710
|
+
agentFolderName
|
|
1711
|
+
});
|
|
1712
|
+
return { success: true, sessionId };
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
console.error("Error starting agent session:", error);
|
|
1715
|
+
throw new Error(error.message || "Failed to start agent session");
|
|
1716
|
+
}
|
|
1717
|
+
});
|
|
1718
|
+
electron.ipcMain.handle("agent:send-message", async (event, sessionId, message) => {
|
|
1719
|
+
try {
|
|
1720
|
+
const mainWindow2 = electron.BrowserWindow.fromWebContents(event.sender);
|
|
1721
|
+
if (!mainWindow2) {
|
|
1722
|
+
throw new Error("No active window found");
|
|
1723
|
+
}
|
|
1724
|
+
await agentManager.sendMessage(sessionId, message, (chunk) => {
|
|
1725
|
+
if (chunk.type === "approval_request") {
|
|
1726
|
+
console.log("📤 [IPC] Sending approval_request chunk to renderer:", chunk.approvalRequest?.requestId);
|
|
1727
|
+
}
|
|
1728
|
+
mainWindow2.webContents.send("agent:message-chunk", sessionId, chunk);
|
|
1729
|
+
});
|
|
1730
|
+
mainWindow2.webContents.send("agent:message-complete", sessionId);
|
|
1731
|
+
return { success: true };
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
console.error("Error sending message to agent:", error);
|
|
1734
|
+
throw new Error(error.message || "Failed to send message");
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
electron.ipcMain.handle("agent:stop-session", async (_, sessionId) => {
|
|
1738
|
+
try {
|
|
1739
|
+
agentManager.stopSession(sessionId);
|
|
1740
|
+
return { success: true };
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
console.error("Error stopping agent session:", error);
|
|
1743
|
+
throw new Error(error.message || "Failed to stop session");
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
electron.ipcMain.handle("agent:is-session-active", async (_, sessionId) => {
|
|
1747
|
+
try {
|
|
1748
|
+
const isActive = agentManager.isSessionActive(sessionId);
|
|
1749
|
+
return { success: true, isActive };
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
console.error("Error checking session status:", error);
|
|
1752
|
+
throw new Error(error.message || "Failed to check session status");
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
electron.ipcMain.handle("agent:set-permission-mode", async (_, sessionId, mode) => {
|
|
1756
|
+
try {
|
|
1757
|
+
agentManager.updateSessionConfig(sessionId, { permissionMode: mode });
|
|
1758
|
+
return { success: true };
|
|
1759
|
+
} catch (error) {
|
|
1760
|
+
console.error("Error setting permission mode:", error);
|
|
1761
|
+
throw new Error(error.message || "Failed to set permission mode");
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
electron.ipcMain.handle("agent:respond-to-approval", async (_, sessionId, requestId, approved) => {
|
|
1765
|
+
try {
|
|
1766
|
+
console.log("📥 [IPC] Received approval response from renderer:", { sessionId, requestId, approved });
|
|
1767
|
+
agentManager.respondToApproval(sessionId, requestId, approved);
|
|
1768
|
+
return { success: true };
|
|
1769
|
+
} catch (error) {
|
|
1770
|
+
console.error("Error responding to approval:", error);
|
|
1771
|
+
throw new Error(error.message || "Failed to respond to approval");
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
electron.ipcMain.handle("keys:has-anthropic", async (_, projectPath2) => {
|
|
1775
|
+
try {
|
|
1776
|
+
const hasKey = await keyManager.hasAnthropicKey(projectPath2);
|
|
1777
|
+
return { success: true, hasKey };
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
console.error("Error checking for API key:", error);
|
|
1780
|
+
throw new Error(error.message || "Failed to check for API key");
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
electron.ipcMain.handle("keys:get-anthropic", async (_, projectPath2) => {
|
|
1784
|
+
try {
|
|
1785
|
+
const apiKey = await keyManager.getAnthropicKey(projectPath2);
|
|
1786
|
+
return { success: true, apiKey };
|
|
1787
|
+
} catch (error) {
|
|
1788
|
+
console.error("Error getting API key:", error);
|
|
1789
|
+
throw new Error(error.message || "Failed to get API key");
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
electron.ipcMain.handle("keys:set-anthropic", async (_, apiKey, projectPath2) => {
|
|
1793
|
+
try {
|
|
1794
|
+
await keyManager.setAnthropicKey(apiKey, projectPath2);
|
|
1795
|
+
return { success: true };
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
console.error("Error setting API key:", error);
|
|
1798
|
+
throw new Error(error.message || "Failed to set API key");
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
electron.ipcMain.handle("keys:delete-anthropic", async (_, projectPath2) => {
|
|
1802
|
+
try {
|
|
1803
|
+
await keyManager.deleteAnthropicKey(projectPath2);
|
|
1804
|
+
return { success: true };
|
|
1805
|
+
} catch (error) {
|
|
1806
|
+
console.error("Error deleting API key:", error);
|
|
1807
|
+
throw new Error(error.message || "Failed to delete API key");
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
electron.ipcMain.handle("auth:check-cli-status", async () => {
|
|
1811
|
+
try {
|
|
1812
|
+
const sdk = await import("@anthropic-ai/claude-agent-sdk");
|
|
1813
|
+
const queryOptions = {
|
|
1814
|
+
// Omit apiKey to use CLI auth
|
|
1815
|
+
systemPrompt: { type: "preset", preset: "claude_code" }
|
|
1816
|
+
};
|
|
1817
|
+
const customCliCommand = await authSettingsManager.getCustomCliCommand();
|
|
1818
|
+
console.log("[AUTH] Custom CLI command:", customCliCommand);
|
|
1819
|
+
console.log("[AUTH] App isPackaged:", electron.app.isPackaged);
|
|
1820
|
+
if (customCliCommand) {
|
|
1821
|
+
console.log("[AUTH] Using custom CLI command:", customCliCommand);
|
|
1822
|
+
queryOptions.pathToClaudeCodeExecutable = customCliCommand;
|
|
1823
|
+
} else if (electron.app.isPackaged) {
|
|
1824
|
+
const path2 = await import("path");
|
|
1825
|
+
const cliPath = path2.join(
|
|
1826
|
+
process.resourcesPath,
|
|
1827
|
+
"app.asar.unpacked",
|
|
1828
|
+
"node_modules",
|
|
1829
|
+
"@anthropic-ai",
|
|
1830
|
+
"claude-agent-sdk",
|
|
1831
|
+
"cli.js"
|
|
1832
|
+
);
|
|
1833
|
+
console.log("[AUTH] Using packaged CLI path:", cliPath);
|
|
1834
|
+
queryOptions.pathToClaudeCodeExecutable = cliPath;
|
|
1835
|
+
} else {
|
|
1836
|
+
console.log('[AUTH] Using default "claude" command from PATH');
|
|
1837
|
+
}
|
|
1838
|
+
const testQuery = sdk.query({
|
|
1839
|
+
prompt: "",
|
|
1840
|
+
options: queryOptions
|
|
1841
|
+
});
|
|
1842
|
+
const accountInfoPromise = testQuery.accountInfo();
|
|
1843
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1844
|
+
setTimeout(() => reject(new Error("CLI authentication check timed out")), 1e4);
|
|
1845
|
+
});
|
|
1846
|
+
const accountInfo = await Promise.race([accountInfoPromise, timeoutPromise]);
|
|
1847
|
+
return {
|
|
1848
|
+
success: true,
|
|
1849
|
+
isAuthenticated: true,
|
|
1850
|
+
accountInfo: {
|
|
1851
|
+
email: accountInfo?.email
|
|
1852
|
+
}
|
|
1853
|
+
};
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
return {
|
|
1856
|
+
success: true,
|
|
1857
|
+
isAuthenticated: false,
|
|
1858
|
+
error: error.message
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
electron.ipcMain.handle("auth:get-preferred-method", async (_, projectPath2) => {
|
|
1863
|
+
try {
|
|
1864
|
+
const method = await authSettingsManager.getPreferredMethod(projectPath2);
|
|
1865
|
+
return { success: true, method };
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
console.error("Error getting preferred auth method:", error);
|
|
1868
|
+
throw new Error(error.message || "Failed to get auth preference");
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
electron.ipcMain.handle(
|
|
1872
|
+
"auth:set-preferred-method",
|
|
1873
|
+
async (_, method, projectPath2) => {
|
|
1874
|
+
try {
|
|
1875
|
+
await authSettingsManager.setPreferredMethod(method, projectPath2);
|
|
1876
|
+
return { success: true };
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
console.error("Error setting preferred auth method:", error);
|
|
1879
|
+
throw new Error(error.message || "Failed to set auth preference");
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
);
|
|
1883
|
+
electron.ipcMain.handle("auth:get-auto-fallback", async () => {
|
|
1884
|
+
try {
|
|
1885
|
+
const enabled = await authSettingsManager.getAutoFallback();
|
|
1886
|
+
return { success: true, enabled };
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
console.error("Error getting auto-fallback setting:", error);
|
|
1889
|
+
throw new Error(error.message || "Failed to get auto-fallback setting");
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
electron.ipcMain.handle("auth:set-auto-fallback", async (_, enabled) => {
|
|
1893
|
+
try {
|
|
1894
|
+
await authSettingsManager.setAutoFallback(enabled);
|
|
1895
|
+
return { success: true };
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
console.error("Error setting auto-fallback:", error);
|
|
1898
|
+
throw new Error(error.message || "Failed to set auto-fallback");
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
electron.ipcMain.handle("auth:get-custom-cli-command", async () => {
|
|
1902
|
+
try {
|
|
1903
|
+
const command = await authSettingsManager.getCustomCliCommand();
|
|
1904
|
+
return { success: true, command };
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
console.error("Error getting custom CLI command:", error);
|
|
1907
|
+
throw new Error(error.message || "Failed to get custom CLI command");
|
|
1908
|
+
}
|
|
1909
|
+
});
|
|
1910
|
+
electron.ipcMain.handle("auth:set-custom-cli-command", async (_, command) => {
|
|
1911
|
+
try {
|
|
1912
|
+
await authSettingsManager.setCustomCliCommand(command);
|
|
1913
|
+
return { success: true };
|
|
1914
|
+
} catch (error) {
|
|
1915
|
+
console.error("Error setting custom CLI command:", error);
|
|
1916
|
+
throw new Error(error.message || "Failed to set custom CLI command");
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
electron.ipcMain.handle("auth:clear-custom-cli-command", async () => {
|
|
1920
|
+
try {
|
|
1921
|
+
await authSettingsManager.clearCustomCliCommand();
|
|
1922
|
+
return { success: true };
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
console.error("Error clearing custom CLI command:", error);
|
|
1925
|
+
throw new Error(error.message || "Failed to clear custom CLI command");
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
electron.ipcMain.handle("logs:load", async (_, projectPath2) => {
|
|
1929
|
+
try {
|
|
1930
|
+
const logs = await logManager.loadLogs(projectPath2);
|
|
1931
|
+
return { success: true, logs };
|
|
1932
|
+
} catch (error) {
|
|
1933
|
+
console.error("Error loading logs:", error);
|
|
1934
|
+
return { success: false, logs: [] };
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
electron.ipcMain.handle("logs:save", async (_, projectPath2, logs) => {
|
|
1938
|
+
try {
|
|
1939
|
+
await logManager.saveLogs(projectPath2, logs);
|
|
1940
|
+
return { success: true };
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
console.error("Error saving logs:", error);
|
|
1943
|
+
throw new Error(error.message || "Failed to save logs");
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
const ensureMemoryFolder = async (folderPath) => {
|
|
1947
|
+
const fullPath = path.join(electron.app.getPath("userData"), folderPath);
|
|
1948
|
+
try {
|
|
1949
|
+
await fs.promises.mkdir(fullPath, { recursive: true });
|
|
1950
|
+
return fullPath;
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
console.error("Error creating memory folder:", error);
|
|
1953
|
+
throw error;
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
electron.ipcMain.handle("list-markdown-files", async (_, folderPath) => {
|
|
1957
|
+
try {
|
|
1958
|
+
const fullPath = await ensureMemoryFolder(folderPath);
|
|
1959
|
+
const files = await fs.promises.readdir(fullPath);
|
|
1960
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
1961
|
+
const fileInfos = await Promise.all(
|
|
1962
|
+
mdFiles.map(async (name) => {
|
|
1963
|
+
const filePath = path.join(fullPath, name);
|
|
1964
|
+
const stats = await fs.promises.stat(filePath);
|
|
1965
|
+
return {
|
|
1966
|
+
name,
|
|
1967
|
+
path: filePath,
|
|
1968
|
+
size: stats.size
|
|
1969
|
+
};
|
|
1970
|
+
})
|
|
1971
|
+
);
|
|
1972
|
+
return fileInfos;
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
console.error("Error listing markdown files:", error);
|
|
1975
|
+
return [];
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
electron.ipcMain.handle(
|
|
1979
|
+
"add-markdown-file",
|
|
1980
|
+
async (_, folderPath, sourcePath, fileName) => {
|
|
1981
|
+
try {
|
|
1982
|
+
const fullPath = await ensureMemoryFolder(folderPath);
|
|
1983
|
+
const destPath = path.join(fullPath, fileName);
|
|
1984
|
+
try {
|
|
1985
|
+
await fs.promises.access(destPath);
|
|
1986
|
+
throw new Error(`File "${fileName}" already exists`);
|
|
1987
|
+
} catch {
|
|
1988
|
+
}
|
|
1989
|
+
await fs.promises.copyFile(sourcePath, destPath);
|
|
1990
|
+
return { success: true };
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
console.error("Error adding markdown file:", error);
|
|
1993
|
+
throw new Error(error.message || "Failed to add file");
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
);
|
|
1997
|
+
electron.ipcMain.handle("delete-markdown-file", async (_, filePath) => {
|
|
1998
|
+
try {
|
|
1999
|
+
await fs.promises.unlink(filePath);
|
|
2000
|
+
return { success: true };
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
console.error("Error deleting markdown file:", error);
|
|
2003
|
+
throw new Error("Failed to delete file");
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
const ensureContextFolder = async (projectPath2) => {
|
|
2007
|
+
const contextPath = path.join(projectPath2, ".claude", "context");
|
|
2008
|
+
try {
|
|
2009
|
+
await fs.promises.mkdir(contextPath, { recursive: true });
|
|
2010
|
+
return contextPath;
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
console.error("Error creating context folder:", error);
|
|
2013
|
+
throw error;
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
const ensureMemoryNodeFolder = async (projectPath2, memoryFolderName) => {
|
|
2017
|
+
const memoryPath = path.join(projectPath2, ".claude", "memory", memoryFolderName);
|
|
2018
|
+
try {
|
|
2019
|
+
await fs.promises.mkdir(memoryPath, { recursive: true });
|
|
2020
|
+
return memoryPath;
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
console.error("Error creating memory node folder:", error);
|
|
2023
|
+
throw error;
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
const getMetadataPath = async (projectPath2) => {
|
|
2027
|
+
const contextPath = await ensureContextFolder(projectPath2);
|
|
2028
|
+
return path.join(contextPath, "_metadata.json");
|
|
2029
|
+
};
|
|
2030
|
+
const getMemoryNodeMetadataPath = async (projectPath2, memoryFolderName) => {
|
|
2031
|
+
const memoryPath = await ensureMemoryNodeFolder(projectPath2, memoryFolderName);
|
|
2032
|
+
return path.join(memoryPath, "_metadata.json");
|
|
2033
|
+
};
|
|
2034
|
+
const loadMetadata = async (projectPath2) => {
|
|
2035
|
+
try {
|
|
2036
|
+
const metadataPath = await getMetadataPath(projectPath2);
|
|
2037
|
+
const content = await fs.promises.readFile(metadataPath, "utf-8");
|
|
2038
|
+
return JSON.parse(content);
|
|
2039
|
+
} catch {
|
|
2040
|
+
return {};
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
2043
|
+
const saveMetadata = async (projectPath2, metadata) => {
|
|
2044
|
+
try {
|
|
2045
|
+
const metadataPath = await getMetadataPath(projectPath2);
|
|
2046
|
+
await fs.promises.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
2047
|
+
} catch (error) {
|
|
2048
|
+
console.error("Error saving metadata:", error);
|
|
2049
|
+
throw error;
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
const getFileType = (fileName) => {
|
|
2053
|
+
const ext = fileName.split(".").pop()?.toLowerCase() || "";
|
|
2054
|
+
if (["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico"].includes(ext)) {
|
|
2055
|
+
return "image";
|
|
2056
|
+
}
|
|
2057
|
+
if (["md", "markdown", "txt"].includes(ext)) {
|
|
2058
|
+
return "text";
|
|
2059
|
+
}
|
|
2060
|
+
if (["js", "ts", "jsx", "tsx", "py", "java", "cpp", "c", "h", "css", "html", "json", "xml", "yaml", "yml", "sh", "bash"].includes(ext)) {
|
|
2061
|
+
return "code";
|
|
2062
|
+
}
|
|
2063
|
+
if (ext === "pdf") {
|
|
2064
|
+
return "pdf";
|
|
2065
|
+
}
|
|
2066
|
+
if (["mp4", "avi", "mov", "wmv", "flv", "webm"].includes(ext)) {
|
|
2067
|
+
return "video";
|
|
2068
|
+
}
|
|
2069
|
+
if (["mp3", "wav", "ogg", "flac", "m4a"].includes(ext)) {
|
|
2070
|
+
return "audio";
|
|
2071
|
+
}
|
|
2072
|
+
if (["zip", "rar", "7z", "tar", "gz"].includes(ext)) {
|
|
2073
|
+
return "archive";
|
|
2074
|
+
}
|
|
2075
|
+
return "other";
|
|
2076
|
+
};
|
|
2077
|
+
electron.ipcMain.handle("list-context-files", async (_, projectPath2) => {
|
|
2078
|
+
try {
|
|
2079
|
+
const contextPath = await ensureContextFolder(projectPath2);
|
|
2080
|
+
const metadata = await loadMetadata(projectPath2);
|
|
2081
|
+
const allFiles = await fs.promises.readdir(contextPath);
|
|
2082
|
+
const files = allFiles.filter((f) => f !== "_metadata.json");
|
|
2083
|
+
const fileInfos = await Promise.all(
|
|
2084
|
+
files.map(async (name) => {
|
|
2085
|
+
const filePath = path.join(contextPath, name);
|
|
2086
|
+
const stats = await fs.promises.stat(filePath);
|
|
2087
|
+
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
2088
|
+
const type = getFileType(name);
|
|
2089
|
+
const fileMeta = metadata[name] || {};
|
|
2090
|
+
return {
|
|
2091
|
+
name,
|
|
2092
|
+
path: filePath,
|
|
2093
|
+
size: stats.size,
|
|
2094
|
+
type,
|
|
2095
|
+
ext,
|
|
2096
|
+
title: fileMeta.title || name,
|
|
2097
|
+
description: fileMeta.description || "",
|
|
2098
|
+
created: fileMeta.created || stats.birthtime.toISOString()
|
|
2099
|
+
};
|
|
2100
|
+
})
|
|
2101
|
+
);
|
|
2102
|
+
fileInfos.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
|
|
2103
|
+
return fileInfos;
|
|
2104
|
+
} catch (error) {
|
|
2105
|
+
console.error("Error listing context files:", error);
|
|
2106
|
+
return [];
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
const walkDirectory = async (dir, projectRoot, depth = 0, maxDepth = 10) => {
|
|
2110
|
+
if (depth > maxDepth) return [];
|
|
2111
|
+
const ignorePatterns = [
|
|
2112
|
+
"node_modules",
|
|
2113
|
+
".git",
|
|
2114
|
+
".claude",
|
|
2115
|
+
"dist",
|
|
2116
|
+
"build",
|
|
2117
|
+
"out",
|
|
2118
|
+
"coverage",
|
|
2119
|
+
".next",
|
|
2120
|
+
".nuxt",
|
|
2121
|
+
".cache",
|
|
2122
|
+
".vscode",
|
|
2123
|
+
".idea",
|
|
2124
|
+
".DS_Store",
|
|
2125
|
+
"thumbs.db",
|
|
2126
|
+
"*.log",
|
|
2127
|
+
".env",
|
|
2128
|
+
".env.local",
|
|
2129
|
+
"__pycache__",
|
|
2130
|
+
"*.pyc",
|
|
2131
|
+
".pytest_cache",
|
|
2132
|
+
"venv",
|
|
2133
|
+
".venv",
|
|
2134
|
+
"target",
|
|
2135
|
+
"pkg",
|
|
2136
|
+
".meta"
|
|
2137
|
+
];
|
|
2138
|
+
const shouldIgnore = (name) => {
|
|
2139
|
+
return ignorePatterns.some((pattern) => {
|
|
2140
|
+
if (pattern.includes("*")) {
|
|
2141
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
|
|
2142
|
+
return regex.test(name);
|
|
2143
|
+
}
|
|
2144
|
+
return name === pattern || name.startsWith(pattern);
|
|
2145
|
+
});
|
|
2146
|
+
};
|
|
2147
|
+
try {
|
|
2148
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
2149
|
+
const results = [];
|
|
2150
|
+
for (const entry of entries) {
|
|
2151
|
+
if (shouldIgnore(entry.name)) continue;
|
|
2152
|
+
const fullPath = path.join(dir, entry.name);
|
|
2153
|
+
const relativePath = fullPath.replace(projectRoot + path.sep, "").replace(/\\/g, "/");
|
|
2154
|
+
if (entry.isDirectory()) {
|
|
2155
|
+
const subResults = await walkDirectory(fullPath, projectRoot, depth + 1, maxDepth);
|
|
2156
|
+
results.push(...subResults);
|
|
2157
|
+
} else if (entry.isFile()) {
|
|
2158
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
2159
|
+
const type = getFileType(entry.name);
|
|
2160
|
+
const includeExtensions = [
|
|
2161
|
+
".js",
|
|
2162
|
+
".ts",
|
|
2163
|
+
".jsx",
|
|
2164
|
+
".tsx",
|
|
2165
|
+
".vue",
|
|
2166
|
+
".py",
|
|
2167
|
+
".java",
|
|
2168
|
+
".cpp",
|
|
2169
|
+
".c",
|
|
2170
|
+
".h",
|
|
2171
|
+
".cs",
|
|
2172
|
+
".go",
|
|
2173
|
+
".rs",
|
|
2174
|
+
".rb",
|
|
2175
|
+
".php",
|
|
2176
|
+
".swift",
|
|
2177
|
+
".kt",
|
|
2178
|
+
".scala",
|
|
2179
|
+
".sh",
|
|
2180
|
+
".html",
|
|
2181
|
+
".css",
|
|
2182
|
+
".scss",
|
|
2183
|
+
".sass",
|
|
2184
|
+
".json",
|
|
2185
|
+
".xml",
|
|
2186
|
+
".yaml",
|
|
2187
|
+
".yml",
|
|
2188
|
+
".md",
|
|
2189
|
+
".markdown",
|
|
2190
|
+
".txt"
|
|
2191
|
+
];
|
|
2192
|
+
const shouldInclude = includeExtensions.includes(ext) || entry.name.match(/^(Dockerfile|Makefile|README|LICENSE|CHANGELOG)/);
|
|
2193
|
+
if (shouldInclude) {
|
|
2194
|
+
results.push({
|
|
2195
|
+
name: entry.name,
|
|
2196
|
+
path: relativePath,
|
|
2197
|
+
type,
|
|
2198
|
+
ext: ext.replace(".", "")
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return results;
|
|
2204
|
+
} catch (error) {
|
|
2205
|
+
console.error(`Error walking directory ${dir}:`, error);
|
|
2206
|
+
return [];
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
electron.ipcMain.handle("list-project-files", async (_, projectPath2) => {
|
|
2210
|
+
try {
|
|
2211
|
+
await fs.promises.access(projectPath2);
|
|
2212
|
+
const files = await walkDirectory(projectPath2, projectPath2);
|
|
2213
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
2214
|
+
const limitedFiles = files.slice(0, 500);
|
|
2215
|
+
if (files.length > 500) {
|
|
2216
|
+
console.warn(`Project has ${files.length} files, showing first 500`);
|
|
2217
|
+
}
|
|
2218
|
+
return limitedFiles;
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
console.error("Error listing project files:", error);
|
|
2221
|
+
return [];
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
electron.ipcMain.handle("add-context-file", async (_, projectPath2, sourcePath, fileName, title, description) => {
|
|
2225
|
+
try {
|
|
2226
|
+
const contextPath = await ensureContextFolder(projectPath2);
|
|
2227
|
+
const destPath = path.join(contextPath, fileName);
|
|
2228
|
+
try {
|
|
2229
|
+
await fs.promises.access(destPath);
|
|
2230
|
+
throw new Error(`File "${fileName}" already exists`);
|
|
2231
|
+
} catch (err) {
|
|
2232
|
+
if (err.code !== "ENOENT") {
|
|
2233
|
+
throw err;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
await fs.promises.copyFile(sourcePath, destPath);
|
|
2237
|
+
const metadata = await loadMetadata(projectPath2);
|
|
2238
|
+
metadata[fileName] = {
|
|
2239
|
+
title: title || fileName,
|
|
2240
|
+
description: description || "",
|
|
2241
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2242
|
+
};
|
|
2243
|
+
await saveMetadata(projectPath2, metadata);
|
|
2244
|
+
return { success: true };
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
console.error("Error adding context file:", error);
|
|
2247
|
+
throw new Error(error.message || "Failed to add file");
|
|
2248
|
+
}
|
|
2249
|
+
});
|
|
2250
|
+
electron.ipcMain.handle("rename-context-file", async (_, projectPath2, oldName, newName) => {
|
|
2251
|
+
try {
|
|
2252
|
+
const contextPath = await ensureContextFolder(projectPath2);
|
|
2253
|
+
const oldPath = path.join(contextPath, oldName);
|
|
2254
|
+
const newPath = path.join(contextPath, newName);
|
|
2255
|
+
try {
|
|
2256
|
+
await fs.promises.access(newPath);
|
|
2257
|
+
throw new Error(`File "${newName}" already exists`);
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
if (err.code !== "ENOENT") {
|
|
2260
|
+
throw err;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
await fs.promises.rename(oldPath, newPath);
|
|
2264
|
+
const metadata = await loadMetadata(projectPath2);
|
|
2265
|
+
if (metadata[oldName]) {
|
|
2266
|
+
metadata[newName] = metadata[oldName];
|
|
2267
|
+
delete metadata[oldName];
|
|
2268
|
+
await saveMetadata(projectPath2, metadata);
|
|
2269
|
+
}
|
|
2270
|
+
return { success: true };
|
|
2271
|
+
} catch (error) {
|
|
2272
|
+
console.error("Error renaming context file:", error);
|
|
2273
|
+
throw new Error(error.message || "Failed to rename file");
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
electron.ipcMain.handle("update-context-metadata", async (_, projectPath2, fileName, title, description) => {
|
|
2277
|
+
try {
|
|
2278
|
+
const metadata = await loadMetadata(projectPath2);
|
|
2279
|
+
if (!metadata[fileName]) {
|
|
2280
|
+
metadata[fileName] = { created: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2281
|
+
}
|
|
2282
|
+
metadata[fileName].title = title;
|
|
2283
|
+
metadata[fileName].description = description;
|
|
2284
|
+
await saveMetadata(projectPath2, metadata);
|
|
2285
|
+
return { success: true };
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
console.error("Error updating context metadata:", error);
|
|
2288
|
+
throw new Error(error.message || "Failed to update metadata");
|
|
2289
|
+
}
|
|
2290
|
+
});
|
|
2291
|
+
electron.ipcMain.handle("delete-context-file", async (_, projectPath2, fileName) => {
|
|
2292
|
+
try {
|
|
2293
|
+
const contextPath = await ensureContextFolder(projectPath2);
|
|
2294
|
+
const filePath = path.join(contextPath, fileName);
|
|
2295
|
+
await fs.promises.unlink(filePath);
|
|
2296
|
+
const metadata = await loadMetadata(projectPath2);
|
|
2297
|
+
if (metadata[fileName]) {
|
|
2298
|
+
delete metadata[fileName];
|
|
2299
|
+
await saveMetadata(projectPath2, metadata);
|
|
2300
|
+
}
|
|
2301
|
+
return { success: true };
|
|
2302
|
+
} catch (error) {
|
|
2303
|
+
console.error("Error deleting context file:", error);
|
|
2304
|
+
throw new Error(error.message || "Failed to delete file");
|
|
2305
|
+
}
|
|
2306
|
+
});
|
|
2307
|
+
electron.ipcMain.handle("read-context-file", async (_, projectPath2, fileName) => {
|
|
2308
|
+
try {
|
|
2309
|
+
const contextPath = await ensureContextFolder(projectPath2);
|
|
2310
|
+
const filePath = path.join(contextPath, fileName);
|
|
2311
|
+
const type = getFileType(fileName);
|
|
2312
|
+
let content = null;
|
|
2313
|
+
let dataUrl = null;
|
|
2314
|
+
if (type === "image") {
|
|
2315
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
2316
|
+
const ext = fileName.split(".").pop()?.toLowerCase() || "png";
|
|
2317
|
+
const mimeType = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
|
|
2318
|
+
dataUrl = `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
2319
|
+
} else if (type === "text" || type === "code") {
|
|
2320
|
+
content = await fs.promises.readFile(filePath, "utf-8");
|
|
2321
|
+
} else {
|
|
2322
|
+
const stats = await fs.promises.stat(filePath);
|
|
2323
|
+
content = `File size: ${(stats.size / 1024).toFixed(2)} KB`;
|
|
2324
|
+
}
|
|
2325
|
+
return {
|
|
2326
|
+
success: true,
|
|
2327
|
+
type,
|
|
2328
|
+
content,
|
|
2329
|
+
dataUrl
|
|
2330
|
+
};
|
|
2331
|
+
} catch (error) {
|
|
2332
|
+
console.error("Error reading context file:", error);
|
|
2333
|
+
throw new Error(error.message || "Failed to read file");
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
const loadMemoryNodeMetadata = async (projectPath2, memoryFolderName) => {
|
|
2337
|
+
try {
|
|
2338
|
+
const metadataPath = await getMemoryNodeMetadataPath(projectPath2, memoryFolderName);
|
|
2339
|
+
const content = await fs.promises.readFile(metadataPath, "utf-8");
|
|
2340
|
+
return JSON.parse(content);
|
|
2341
|
+
} catch {
|
|
2342
|
+
return {};
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
const saveMemoryNodeMetadata = async (projectPath2, memoryFolderName, metadata) => {
|
|
2346
|
+
try {
|
|
2347
|
+
const metadataPath = await getMemoryNodeMetadataPath(projectPath2, memoryFolderName);
|
|
2348
|
+
await fs.promises.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
2349
|
+
} catch (error) {
|
|
2350
|
+
console.error("Error saving memory node metadata:", error);
|
|
2351
|
+
throw error;
|
|
2352
|
+
}
|
|
2353
|
+
};
|
|
2354
|
+
electron.ipcMain.handle("list-memory-node-files", async (_, projectPath2, memoryFolderName) => {
|
|
2355
|
+
try {
|
|
2356
|
+
const memoryPath = await ensureMemoryNodeFolder(projectPath2, memoryFolderName);
|
|
2357
|
+
const metadata = await loadMemoryNodeMetadata(projectPath2, memoryFolderName);
|
|
2358
|
+
const allFiles = await fs.promises.readdir(memoryPath);
|
|
2359
|
+
const files = allFiles.filter((f) => f !== "_metadata.json");
|
|
2360
|
+
const fileInfos = await Promise.all(
|
|
2361
|
+
files.map(async (name) => {
|
|
2362
|
+
const filePath = path.join(memoryPath, name);
|
|
2363
|
+
const stats = await fs.promises.stat(filePath);
|
|
2364
|
+
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
2365
|
+
const type = getFileType(name);
|
|
2366
|
+
const fileMeta = metadata[name] || {};
|
|
2367
|
+
return {
|
|
2368
|
+
name,
|
|
2369
|
+
path: filePath,
|
|
2370
|
+
size: stats.size,
|
|
2371
|
+
type,
|
|
2372
|
+
ext,
|
|
2373
|
+
title: fileMeta.title || name,
|
|
2374
|
+
description: fileMeta.description || "",
|
|
2375
|
+
created: fileMeta.created || stats.birthtime.toISOString()
|
|
2376
|
+
};
|
|
2377
|
+
})
|
|
2378
|
+
);
|
|
2379
|
+
fileInfos.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
|
|
2380
|
+
return fileInfos;
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
console.error("Error listing memory node files:", error);
|
|
2383
|
+
return [];
|
|
2384
|
+
}
|
|
2385
|
+
});
|
|
2386
|
+
electron.ipcMain.handle("add-memory-node-file", async (_, projectPath2, memoryFolderName, sourcePath, fileName, title, description) => {
|
|
2387
|
+
try {
|
|
2388
|
+
const memoryPath = await ensureMemoryNodeFolder(projectPath2, memoryFolderName);
|
|
2389
|
+
const destPath = path.join(memoryPath, fileName);
|
|
2390
|
+
try {
|
|
2391
|
+
await fs.promises.access(destPath);
|
|
2392
|
+
throw new Error(`File "${fileName}" already exists`);
|
|
2393
|
+
} catch (err) {
|
|
2394
|
+
if (err.code !== "ENOENT") {
|
|
2395
|
+
throw err;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
await fs.promises.copyFile(sourcePath, destPath);
|
|
2399
|
+
const metadata = await loadMemoryNodeMetadata(projectPath2, memoryFolderName);
|
|
2400
|
+
metadata[fileName] = {
|
|
2401
|
+
title: title || fileName,
|
|
2402
|
+
description: description || "",
|
|
2403
|
+
created: (/* @__PURE__ */ new Date()).toISOString()
|
|
2404
|
+
};
|
|
2405
|
+
await saveMemoryNodeMetadata(projectPath2, memoryFolderName, metadata);
|
|
2406
|
+
return { success: true };
|
|
2407
|
+
} catch (error) {
|
|
2408
|
+
console.error("Error adding memory node file:", error);
|
|
2409
|
+
throw new Error(error.message || "Failed to add file");
|
|
2410
|
+
}
|
|
2411
|
+
});
|
|
2412
|
+
electron.ipcMain.handle("rename-memory-node-file", async (_, projectPath2, memoryFolderName, oldName, newName) => {
|
|
2413
|
+
try {
|
|
2414
|
+
const memoryPath = await ensureMemoryNodeFolder(projectPath2, memoryFolderName);
|
|
2415
|
+
const oldPath = path.join(memoryPath, oldName);
|
|
2416
|
+
const newPath = path.join(memoryPath, newName);
|
|
2417
|
+
try {
|
|
2418
|
+
await fs.promises.access(newPath);
|
|
2419
|
+
throw new Error(`File "${newName}" already exists`);
|
|
2420
|
+
} catch (err) {
|
|
2421
|
+
if (err.code !== "ENOENT") {
|
|
2422
|
+
throw err;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
await fs.promises.rename(oldPath, newPath);
|
|
2426
|
+
const metadata = await loadMemoryNodeMetadata(projectPath2, memoryFolderName);
|
|
2427
|
+
if (metadata[oldName]) {
|
|
2428
|
+
metadata[newName] = metadata[oldName];
|
|
2429
|
+
delete metadata[oldName];
|
|
2430
|
+
await saveMemoryNodeMetadata(projectPath2, memoryFolderName, metadata);
|
|
2431
|
+
}
|
|
2432
|
+
return { success: true };
|
|
2433
|
+
} catch (error) {
|
|
2434
|
+
console.error("Error renaming memory node file:", error);
|
|
2435
|
+
throw new Error(error.message || "Failed to rename file");
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2438
|
+
electron.ipcMain.handle("update-memory-node-metadata", async (_, projectPath2, memoryFolderName, fileName, title, description) => {
|
|
2439
|
+
try {
|
|
2440
|
+
const metadata = await loadMemoryNodeMetadata(projectPath2, memoryFolderName);
|
|
2441
|
+
if (!metadata[fileName]) {
|
|
2442
|
+
metadata[fileName] = { created: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2443
|
+
}
|
|
2444
|
+
metadata[fileName].title = title;
|
|
2445
|
+
metadata[fileName].description = description;
|
|
2446
|
+
await saveMemoryNodeMetadata(projectPath2, memoryFolderName, metadata);
|
|
2447
|
+
return { success: true };
|
|
2448
|
+
} catch (error) {
|
|
2449
|
+
console.error("Error updating memory node metadata:", error);
|
|
2450
|
+
throw new Error(error.message || "Failed to update metadata");
|
|
2451
|
+
}
|
|
2452
|
+
});
|
|
2453
|
+
electron.ipcMain.handle("delete-memory-node-file", async (_, projectPath2, memoryFolderName, fileName) => {
|
|
2454
|
+
try {
|
|
2455
|
+
const memoryPath = await ensureMemoryNodeFolder(projectPath2, memoryFolderName);
|
|
2456
|
+
const filePath = path.join(memoryPath, fileName);
|
|
2457
|
+
await fs.promises.unlink(filePath);
|
|
2458
|
+
const metadata = await loadMemoryNodeMetadata(projectPath2, memoryFolderName);
|
|
2459
|
+
if (metadata[fileName]) {
|
|
2460
|
+
delete metadata[fileName];
|
|
2461
|
+
await saveMemoryNodeMetadata(projectPath2, memoryFolderName, metadata);
|
|
2462
|
+
}
|
|
2463
|
+
return { success: true };
|
|
2464
|
+
} catch (error) {
|
|
2465
|
+
console.error("Error deleting memory node file:", error);
|
|
2466
|
+
throw new Error(error.message || "Failed to delete file");
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
electron.ipcMain.handle("read-memory-node-file", async (_, projectPath2, memoryFolderName, fileName) => {
|
|
2470
|
+
try {
|
|
2471
|
+
const memoryPath = await ensureMemoryNodeFolder(projectPath2, memoryFolderName);
|
|
2472
|
+
const filePath = path.join(memoryPath, fileName);
|
|
2473
|
+
const type = getFileType(fileName);
|
|
2474
|
+
let content = null;
|
|
2475
|
+
let dataUrl = null;
|
|
2476
|
+
if (type === "image") {
|
|
2477
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
2478
|
+
const ext = fileName.split(".").pop()?.toLowerCase() || "png";
|
|
2479
|
+
const mimeType = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
|
|
2480
|
+
dataUrl = `data:${mimeType};base64,${buffer.toString("base64")}`;
|
|
2481
|
+
} else if (type === "text" || type === "code") {
|
|
2482
|
+
content = await fs.promises.readFile(filePath, "utf-8");
|
|
2483
|
+
} else {
|
|
2484
|
+
const stats = await fs.promises.stat(filePath);
|
|
2485
|
+
content = `File size: ${(stats.size / 1024).toFixed(2)} KB`;
|
|
2486
|
+
}
|
|
2487
|
+
return {
|
|
2488
|
+
success: true,
|
|
2489
|
+
type,
|
|
2490
|
+
content,
|
|
2491
|
+
dataUrl
|
|
2492
|
+
};
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
console.error("Error reading memory node file:", error);
|
|
2495
|
+
throw new Error(error.message || "Failed to read file");
|
|
2496
|
+
}
|
|
2497
|
+
});
|
|
2498
|
+
electron.ipcMain.handle("delete-memory-node-folder", async (_, projectPath2, memoryFolderName) => {
|
|
2499
|
+
try {
|
|
2500
|
+
const memoryPath = path.join(projectPath2, ".claude", "memory", memoryFolderName);
|
|
2501
|
+
try {
|
|
2502
|
+
await fs.promises.access(memoryPath);
|
|
2503
|
+
} catch {
|
|
2504
|
+
return { success: true };
|
|
2505
|
+
}
|
|
2506
|
+
await fs.promises.rm(memoryPath, { recursive: true, force: true });
|
|
2507
|
+
return { success: true };
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
console.error("Error deleting memory node folder:", error);
|
|
2510
|
+
throw new Error(error.message || "Failed to delete memory node folder");
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
const ensureSkillFolder = async (projectPath2, skillName) => {
|
|
2514
|
+
const skillFolder = path.join(projectPath2, ".claude", "skills", skillName);
|
|
2515
|
+
try {
|
|
2516
|
+
await fs.promises.mkdir(skillFolder, { recursive: true });
|
|
2517
|
+
await fs.promises.mkdir(path.join(skillFolder, "scripts"), { recursive: true });
|
|
2518
|
+
await fs.promises.mkdir(path.join(skillFolder, "references"), { recursive: true });
|
|
2519
|
+
await fs.promises.mkdir(path.join(skillFolder, "assets"), { recursive: true });
|
|
2520
|
+
return skillFolder;
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
console.error("Error creating skill folder:", error);
|
|
2523
|
+
throw error;
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
electron.ipcMain.handle("initialize-skill", async (_, projectPath2, skillName) => {
|
|
2527
|
+
try {
|
|
2528
|
+
const skillFolder = await ensureSkillFolder(projectPath2, skillName);
|
|
2529
|
+
const skillMdPath = path.join(skillFolder, "SKILL.md");
|
|
2530
|
+
try {
|
|
2531
|
+
await fs.promises.access(skillMdPath);
|
|
2532
|
+
} catch {
|
|
2533
|
+
const defaultContent = `---
|
|
2534
|
+
name: ${skillName}
|
|
2535
|
+
description: Custom skill for specialized tasks
|
|
2536
|
+
---
|
|
2537
|
+
|
|
2538
|
+
# ${skillName}
|
|
2539
|
+
|
|
2540
|
+
## Overview
|
|
2541
|
+
|
|
2542
|
+
Describe what this skill does and when to use it.
|
|
2543
|
+
|
|
2544
|
+
## Instructions
|
|
2545
|
+
|
|
2546
|
+
1. Step one
|
|
2547
|
+
2. Step two
|
|
2548
|
+
3. Step three
|
|
2549
|
+
`;
|
|
2550
|
+
await fs.promises.writeFile(skillMdPath, defaultContent, "utf-8");
|
|
2551
|
+
}
|
|
2552
|
+
return { success: true };
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
console.error("Error initializing skill:", error);
|
|
2555
|
+
throw new Error("Failed to initialize skill");
|
|
2556
|
+
}
|
|
2557
|
+
});
|
|
2558
|
+
electron.ipcMain.handle("load-skill", async (_, projectPath2, skillName) => {
|
|
2559
|
+
try {
|
|
2560
|
+
const skillFolder = await ensureSkillFolder(projectPath2, skillName);
|
|
2561
|
+
const skillMdPath = path.join(skillFolder, "SKILL.md");
|
|
2562
|
+
let skillMdContent = "";
|
|
2563
|
+
try {
|
|
2564
|
+
skillMdContent = await fs.promises.readFile(skillMdPath, "utf-8");
|
|
2565
|
+
} catch {
|
|
2566
|
+
skillMdContent = `---
|
|
2567
|
+
name: ${skillName}
|
|
2568
|
+
description: Custom skill for specialized tasks
|
|
2569
|
+
---
|
|
2570
|
+
|
|
2571
|
+
# ${skillName}
|
|
2572
|
+
|
|
2573
|
+
## Overview
|
|
2574
|
+
|
|
2575
|
+
Describe what this skill does and when to use it.
|
|
2576
|
+
|
|
2577
|
+
## Instructions
|
|
2578
|
+
|
|
2579
|
+
1. Step one
|
|
2580
|
+
2. Step two
|
|
2581
|
+
3. Step three
|
|
2582
|
+
`;
|
|
2583
|
+
await fs.promises.writeFile(skillMdPath, skillMdContent, "utf-8");
|
|
2584
|
+
}
|
|
2585
|
+
const listFilesInFolder = async (folderPath, type) => {
|
|
2586
|
+
try {
|
|
2587
|
+
const files = await fs.promises.readdir(folderPath);
|
|
2588
|
+
return await Promise.all(
|
|
2589
|
+
files.map(async (name) => {
|
|
2590
|
+
const filePath = path.join(folderPath, name);
|
|
2591
|
+
const stats = await fs.promises.stat(filePath);
|
|
2592
|
+
if (stats.isFile()) {
|
|
2593
|
+
let language = void 0;
|
|
2594
|
+
if (name.endsWith(".py")) language = "python";
|
|
2595
|
+
else if (name.endsWith(".sh") || name.endsWith(".bash")) language = "bash";
|
|
2596
|
+
else if (name.endsWith(".md")) language = "markdown";
|
|
2597
|
+
else if (name.endsWith(".json")) language = "json";
|
|
2598
|
+
else if (name.endsWith(".csv")) language = "csv";
|
|
2599
|
+
return {
|
|
2600
|
+
name,
|
|
2601
|
+
path: filePath,
|
|
2602
|
+
type,
|
|
2603
|
+
language,
|
|
2604
|
+
size: stats.size
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
return null;
|
|
2608
|
+
})
|
|
2609
|
+
).then((results) => results.filter((r) => r !== null));
|
|
2610
|
+
} catch {
|
|
2611
|
+
return [];
|
|
2612
|
+
}
|
|
2613
|
+
};
|
|
2614
|
+
const scripts = await listFilesInFolder(path.join(skillFolder, "scripts"), "script");
|
|
2615
|
+
const references = await listFilesInFolder(path.join(skillFolder, "references"), "reference");
|
|
2616
|
+
const assets = await listFilesInFolder(path.join(skillFolder, "assets"), "asset");
|
|
2617
|
+
return {
|
|
2618
|
+
skillMdContent,
|
|
2619
|
+
scripts,
|
|
2620
|
+
references,
|
|
2621
|
+
assets
|
|
2622
|
+
};
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
console.error("Error loading skill:", error);
|
|
2625
|
+
throw new Error("Failed to load skill");
|
|
2626
|
+
}
|
|
2627
|
+
});
|
|
2628
|
+
electron.ipcMain.handle("save-skill-md", async (_, projectPath2, skillName, content) => {
|
|
2629
|
+
try {
|
|
2630
|
+
const skillFolder = await ensureSkillFolder(projectPath2, skillName);
|
|
2631
|
+
const skillMdPath = path.join(skillFolder, "SKILL.md");
|
|
2632
|
+
await fs.promises.writeFile(skillMdPath, content, "utf-8");
|
|
2633
|
+
return { success: true };
|
|
2634
|
+
} catch (error) {
|
|
2635
|
+
console.error("Error saving SKILL.md:", error);
|
|
2636
|
+
throw new Error("Failed to save SKILL.md");
|
|
2637
|
+
}
|
|
2638
|
+
});
|
|
2639
|
+
electron.ipcMain.handle("select-file", async () => {
|
|
2640
|
+
try {
|
|
2641
|
+
const result = await electron.dialog.showOpenDialog({
|
|
2642
|
+
properties: ["openFile"],
|
|
2643
|
+
title: "Select File",
|
|
2644
|
+
buttonLabel: "Select"
|
|
2645
|
+
});
|
|
2646
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
2647
|
+
return { canceled: true };
|
|
2648
|
+
}
|
|
2649
|
+
const filePath = result.filePaths[0];
|
|
2650
|
+
const fileName = filePath.split(/[/\\]/).pop() || "file";
|
|
2651
|
+
return {
|
|
2652
|
+
canceled: false,
|
|
2653
|
+
path: filePath,
|
|
2654
|
+
name: fileName
|
|
2655
|
+
};
|
|
2656
|
+
} catch (error) {
|
|
2657
|
+
console.error("Error selecting file:", error);
|
|
2658
|
+
throw new Error("Failed to select file");
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
electron.ipcMain.handle("open-path", async (_, path2) => {
|
|
2662
|
+
try {
|
|
2663
|
+
const result = await electron.shell.openPath(path2);
|
|
2664
|
+
if (result) {
|
|
2665
|
+
throw new Error(result);
|
|
2666
|
+
}
|
|
2667
|
+
return { success: true };
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
console.error("Error opening path:", error);
|
|
2670
|
+
throw new Error("Failed to open path");
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
electron.ipcMain.handle("app:clear-cache", async () => {
|
|
2674
|
+
try {
|
|
2675
|
+
const { session } = require("electron");
|
|
2676
|
+
await session.defaultSession.clearStorageData();
|
|
2677
|
+
await session.defaultSession.clearCache();
|
|
2678
|
+
return { success: true };
|
|
2679
|
+
} catch (error) {
|
|
2680
|
+
console.error("Error clearing cache:", error);
|
|
2681
|
+
throw new Error("Failed to clear cache");
|
|
2682
|
+
}
|
|
2683
|
+
});
|
|
2684
|
+
electron.ipcMain.handle("app:restart", async () => {
|
|
2685
|
+
electron.app.relaunch();
|
|
2686
|
+
electron.app.exit(0);
|
|
2687
|
+
});
|
|
2688
|
+
electron.ipcMain.handle("music:ensure-workspace-music", async (_, workspacePath) => {
|
|
2689
|
+
try {
|
|
2690
|
+
if (!workspacePath) throw new Error("workspacePath is required");
|
|
2691
|
+
const musicDir = getWorkspaceMusicDir(workspacePath);
|
|
2692
|
+
await fs.promises.mkdir(musicDir, { recursive: true });
|
|
2693
|
+
return { success: true, path: musicDir };
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
console.error("Error ensuring workspace music folder:", error);
|
|
2696
|
+
throw new Error(error.message || "Failed to ensure music folder");
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
2699
|
+
electron.ipcMain.handle("music:get-workspace-music-path", async (_, workspacePath) => {
|
|
2700
|
+
if (!workspacePath) return "";
|
|
2701
|
+
return getWorkspaceMusicDir(workspacePath);
|
|
2702
|
+
});
|
|
2703
|
+
electron.ipcMain.handle("music:select-tracks", async () => {
|
|
2704
|
+
try {
|
|
2705
|
+
const result = await electron.dialog.showOpenDialog({
|
|
2706
|
+
properties: ["openFile", "multiSelections"],
|
|
2707
|
+
title: "Select Music Files",
|
|
2708
|
+
buttonLabel: "Add to Playlist",
|
|
2709
|
+
filters: [
|
|
2710
|
+
{ name: "Audio", extensions: ["mp3", "wav", "m4a", "ogg"] },
|
|
2711
|
+
{ name: "All Files", extensions: ["*"] }
|
|
2712
|
+
]
|
|
2713
|
+
});
|
|
2714
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
2715
|
+
return { canceled: true };
|
|
2716
|
+
}
|
|
2717
|
+
return { canceled: false, paths: result.filePaths };
|
|
2718
|
+
} catch (error) {
|
|
2719
|
+
console.error("Error selecting tracks:", error);
|
|
2720
|
+
throw new Error("Failed to select tracks");
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2723
|
+
electron.ipcMain.handle("music:add-tracks", async (_, workspacePath, sourcePaths) => {
|
|
2724
|
+
try {
|
|
2725
|
+
if (!workspacePath) throw new Error("workspacePath is required");
|
|
2726
|
+
if (!Array.isArray(sourcePaths) || sourcePaths.length === 0) return { success: true, added: 0 };
|
|
2727
|
+
const musicDir = getWorkspaceMusicDir(workspacePath);
|
|
2728
|
+
await fs.promises.mkdir(musicDir, { recursive: true });
|
|
2729
|
+
let added = 0;
|
|
2730
|
+
for (const sourcePath of sourcePaths) {
|
|
2731
|
+
const ext = path.extname(sourcePath).toLowerCase();
|
|
2732
|
+
if (!SUPPORTED_AUDIO_EXTS.has(ext)) continue;
|
|
2733
|
+
const base = path.basename(sourcePath);
|
|
2734
|
+
const nameNoExt = base.slice(0, -ext.length);
|
|
2735
|
+
let destName = base;
|
|
2736
|
+
let destPath = path.join(musicDir, destName);
|
|
2737
|
+
let n = 1;
|
|
2738
|
+
while (true) {
|
|
2739
|
+
try {
|
|
2740
|
+
await fs.promises.access(destPath);
|
|
2741
|
+
destName = `${nameNoExt} (${n})${ext}`;
|
|
2742
|
+
destPath = path.join(musicDir, destName);
|
|
2743
|
+
n++;
|
|
2744
|
+
} catch {
|
|
2745
|
+
break;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
await fs.promises.copyFile(sourcePath, destPath);
|
|
2749
|
+
added++;
|
|
2750
|
+
}
|
|
2751
|
+
return { success: true, added };
|
|
2752
|
+
} catch (error) {
|
|
2753
|
+
console.error("Error adding tracks:", error);
|
|
2754
|
+
throw new Error(error.message || "Failed to add tracks");
|
|
2755
|
+
}
|
|
2756
|
+
});
|
|
2757
|
+
electron.ipcMain.handle("music:list-tracks", async (_, workspacePath) => {
|
|
2758
|
+
try {
|
|
2759
|
+
const defaultDir = getDefaultMusicDir();
|
|
2760
|
+
const defaultFiles = await listAudioFilesInDir(defaultDir);
|
|
2761
|
+
const workspaceDir = workspacePath ? getWorkspaceMusicDir(workspacePath) : "";
|
|
2762
|
+
const workspaceFiles = workspaceDir ? await listAudioFilesInDir(workspaceDir) : [];
|
|
2763
|
+
const tracks = [
|
|
2764
|
+
...defaultFiles.map((filePath) => ({
|
|
2765
|
+
id: `default:${filePath}`,
|
|
2766
|
+
title: fileTitleFromPath(filePath),
|
|
2767
|
+
source: "default",
|
|
2768
|
+
filePath,
|
|
2769
|
+
url: url.pathToFileURL(filePath).toString()
|
|
2770
|
+
})),
|
|
2771
|
+
...workspaceFiles.map((filePath) => ({
|
|
2772
|
+
id: `workspace:${filePath}`,
|
|
2773
|
+
title: fileTitleFromPath(filePath),
|
|
2774
|
+
source: "workspace",
|
|
2775
|
+
filePath,
|
|
2776
|
+
url: url.pathToFileURL(filePath).toString()
|
|
2777
|
+
}))
|
|
2778
|
+
];
|
|
2779
|
+
return tracks;
|
|
2780
|
+
} catch (error) {
|
|
2781
|
+
console.error("Error listing tracks:", error);
|
|
2782
|
+
return [];
|
|
2783
|
+
}
|
|
2784
|
+
});
|
|
2785
|
+
electron.ipcMain.handle("music:read-track", async (_, filePath) => {
|
|
2786
|
+
try {
|
|
2787
|
+
if (!filePath) throw new Error("filePath is required");
|
|
2788
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2789
|
+
if (!SUPPORTED_AUDIO_EXTS.has(ext)) {
|
|
2790
|
+
throw new Error(`Unsupported audio type: ${ext}`);
|
|
2791
|
+
}
|
|
2792
|
+
await fs.promises.access(filePath);
|
|
2793
|
+
const bytes = await fs.promises.readFile(filePath);
|
|
2794
|
+
const mime = ext === ".mp3" ? "audio/mpeg" : ext === ".wav" ? "audio/wav" : ext === ".m4a" ? "audio/mp4" : ext === ".ogg" ? "audio/ogg" : "application/octet-stream";
|
|
2795
|
+
return { success: true, bytes, mime };
|
|
2796
|
+
} catch (error) {
|
|
2797
|
+
console.error("Error reading track:", error);
|
|
2798
|
+
return { success: false, error: error.message || "Failed to read track" };
|
|
2799
|
+
}
|
|
2800
|
+
});
|
|
2801
|
+
electron.ipcMain.handle(
|
|
2802
|
+
"add-skill-file",
|
|
2803
|
+
async (_, projectPath2, skillName, sourcePath, fileName, fileType) => {
|
|
2804
|
+
try {
|
|
2805
|
+
const skillFolder = await ensureSkillFolder(projectPath2, skillName);
|
|
2806
|
+
let destFolder = skillFolder;
|
|
2807
|
+
if (fileType === "script") destFolder = path.join(skillFolder, "scripts");
|
|
2808
|
+
else if (fileType === "reference") destFolder = path.join(skillFolder, "references");
|
|
2809
|
+
else if (fileType === "asset") destFolder = path.join(skillFolder, "assets");
|
|
2810
|
+
const destPath = path.join(destFolder, fileName);
|
|
2811
|
+
try {
|
|
2812
|
+
await fs.promises.access(destPath);
|
|
2813
|
+
throw new Error(`File "${fileName}" already exists`);
|
|
2814
|
+
} catch {
|
|
2815
|
+
}
|
|
2816
|
+
await fs.promises.copyFile(sourcePath, destPath);
|
|
2817
|
+
const stats = await fs.promises.stat(destPath);
|
|
2818
|
+
let language = void 0;
|
|
2819
|
+
if (fileName.endsWith(".py")) language = "python";
|
|
2820
|
+
else if (fileName.endsWith(".sh") || fileName.endsWith(".bash")) language = "bash";
|
|
2821
|
+
else if (fileName.endsWith(".md")) language = "markdown";
|
|
2822
|
+
else if (fileName.endsWith(".json")) language = "json";
|
|
2823
|
+
else if (fileName.endsWith(".csv")) language = "csv";
|
|
2824
|
+
return {
|
|
2825
|
+
success: true,
|
|
2826
|
+
file: {
|
|
2827
|
+
name: fileName,
|
|
2828
|
+
path: destPath,
|
|
2829
|
+
type: fileType,
|
|
2830
|
+
language,
|
|
2831
|
+
size: stats.size
|
|
2832
|
+
}
|
|
2833
|
+
};
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
console.error("Error adding skill file:", error);
|
|
2836
|
+
throw new Error(error.message || "Failed to add file");
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
);
|
|
2840
|
+
electron.ipcMain.handle("delete-skill-file", async (_, filePath) => {
|
|
2841
|
+
try {
|
|
2842
|
+
await fs.promises.unlink(filePath);
|
|
2843
|
+
return { success: true };
|
|
2844
|
+
} catch (error) {
|
|
2845
|
+
console.error("Error deleting skill file:", error);
|
|
2846
|
+
throw new Error("Failed to delete file");
|
|
2847
|
+
}
|
|
2848
|
+
});
|
|
2849
|
+
electron.ipcMain.handle("delete-skill-folder", async (_, projectPath2, skillName) => {
|
|
2850
|
+
try {
|
|
2851
|
+
const skillFolder = path.join(projectPath2, ".claude", "skills", skillName);
|
|
2852
|
+
await fs.promises.rm(skillFolder, { recursive: true, force: true });
|
|
2853
|
+
return { success: true };
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
console.error("Error deleting skill folder:", error);
|
|
2856
|
+
throw new Error("Failed to delete skill folder");
|
|
2857
|
+
}
|
|
2858
|
+
});
|
|
2859
|
+
electron.ipcMain.handle("scan-skills-folder", async (_, projectPath2) => {
|
|
2860
|
+
try {
|
|
2861
|
+
const skillsFolder = path.join(projectPath2, ".claude", "skills");
|
|
2862
|
+
try {
|
|
2863
|
+
await fs.promises.access(skillsFolder);
|
|
2864
|
+
} catch {
|
|
2865
|
+
return { skills: [] };
|
|
2866
|
+
}
|
|
2867
|
+
const entries = await fs.promises.readdir(skillsFolder, { withFileTypes: true });
|
|
2868
|
+
const skillFolders = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2869
|
+
return { skills: skillFolders };
|
|
2870
|
+
} catch (error) {
|
|
2871
|
+
console.error("Error scanning skills folder:", error);
|
|
2872
|
+
throw new Error("Failed to scan skills folder");
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
const ensureSubagentFolder = async (projectPath2) => {
|
|
2876
|
+
const subagentPath = path.join(projectPath2, ".claude", "agents");
|
|
2877
|
+
try {
|
|
2878
|
+
await fs.promises.mkdir(subagentPath, { recursive: true });
|
|
2879
|
+
return subagentPath;
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
console.error("Error creating subagent folder:", error);
|
|
2882
|
+
throw error;
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
const parseSubagentFile = (content) => {
|
|
2886
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
2887
|
+
const match = content.match(frontmatterRegex);
|
|
2888
|
+
if (match) {
|
|
2889
|
+
try {
|
|
2890
|
+
const frontmatter = yaml.load(match[1]);
|
|
2891
|
+
const markdown = match[2].trim();
|
|
2892
|
+
return { frontmatter, markdown };
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
console.error("Error parsing YAML frontmatter:", error);
|
|
2895
|
+
throw new Error("Invalid YAML frontmatter");
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return { frontmatter: {}, markdown: content.trim() };
|
|
2899
|
+
};
|
|
2900
|
+
const generateSubagentFile = (frontmatter, markdown) => {
|
|
2901
|
+
const yamlString = yaml.dump(frontmatter, { lineWidth: -1, noRefs: true });
|
|
2902
|
+
return `---
|
|
2903
|
+
${yamlString}---
|
|
2904
|
+
|
|
2905
|
+
${markdown}`;
|
|
2906
|
+
};
|
|
2907
|
+
electron.ipcMain.handle("list-subagent-files", async (_, projectPath2) => {
|
|
2908
|
+
try {
|
|
2909
|
+
const subagentPath = await ensureSubagentFolder(projectPath2);
|
|
2910
|
+
const allFiles = await fs.promises.readdir(subagentPath);
|
|
2911
|
+
const mdFiles = allFiles.filter((f) => f.endsWith(".md"));
|
|
2912
|
+
const fileInfos = await Promise.all(
|
|
2913
|
+
mdFiles.map(async (fileName) => {
|
|
2914
|
+
const filePath = path.join(subagentPath, fileName);
|
|
2915
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
2916
|
+
const stats = await fs.promises.stat(filePath);
|
|
2917
|
+
const { frontmatter } = parseSubagentFile(content);
|
|
2918
|
+
return {
|
|
2919
|
+
fileName,
|
|
2920
|
+
path: filePath,
|
|
2921
|
+
name: frontmatter.name || fileName.replace(".md", ""),
|
|
2922
|
+
description: frontmatter.description || "",
|
|
2923
|
+
tools: frontmatter.tools || "",
|
|
2924
|
+
model: frontmatter.model || "",
|
|
2925
|
+
permissionMode: frontmatter.permissionMode || "",
|
|
2926
|
+
skills: frontmatter.skills || "",
|
|
2927
|
+
created: stats.birthtime.toISOString(),
|
|
2928
|
+
modified: stats.mtime.toISOString()
|
|
2929
|
+
};
|
|
2930
|
+
})
|
|
2931
|
+
);
|
|
2932
|
+
fileInfos.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
2933
|
+
return fileInfos;
|
|
2934
|
+
} catch (error) {
|
|
2935
|
+
console.error("Error listing subagent files:", error);
|
|
2936
|
+
return [];
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
electron.ipcMain.handle("read-subagent-file", async (_, projectPath2, fileName) => {
|
|
2940
|
+
try {
|
|
2941
|
+
const subagentPath = await ensureSubagentFolder(projectPath2);
|
|
2942
|
+
const filePath = path.join(subagentPath, fileName);
|
|
2943
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
2944
|
+
const { frontmatter, markdown } = parseSubagentFile(content);
|
|
2945
|
+
return {
|
|
2946
|
+
success: true,
|
|
2947
|
+
fileName,
|
|
2948
|
+
frontmatter: {
|
|
2949
|
+
name: frontmatter.name || "",
|
|
2950
|
+
description: frontmatter.description || "",
|
|
2951
|
+
tools: frontmatter.tools || "",
|
|
2952
|
+
model: frontmatter.model || "",
|
|
2953
|
+
permissionMode: frontmatter.permissionMode || "",
|
|
2954
|
+
skills: frontmatter.skills || ""
|
|
2955
|
+
},
|
|
2956
|
+
markdown
|
|
2957
|
+
};
|
|
2958
|
+
} catch (error) {
|
|
2959
|
+
if (error.code !== "ENOENT") {
|
|
2960
|
+
console.error("Error reading subagent file:", error);
|
|
2961
|
+
}
|
|
2962
|
+
throw new Error(error.message || "Failed to read subagent file");
|
|
2963
|
+
}
|
|
2964
|
+
});
|
|
2965
|
+
electron.ipcMain.handle("write-subagent-file", async (_, projectPath2, fileName, frontmatter, markdown) => {
|
|
2966
|
+
try {
|
|
2967
|
+
const subagentPath = await ensureSubagentFolder(projectPath2);
|
|
2968
|
+
const filePath = path.join(subagentPath, fileName);
|
|
2969
|
+
const cleanedFrontmatter = {
|
|
2970
|
+
name: frontmatter.name,
|
|
2971
|
+
description: frontmatter.description
|
|
2972
|
+
};
|
|
2973
|
+
if (frontmatter.tools && frontmatter.tools.trim()) {
|
|
2974
|
+
cleanedFrontmatter["tools"] = frontmatter.tools;
|
|
2975
|
+
}
|
|
2976
|
+
if (frontmatter.model && frontmatter.model.trim()) {
|
|
2977
|
+
cleanedFrontmatter["model"] = frontmatter.model;
|
|
2978
|
+
}
|
|
2979
|
+
if (frontmatter.permissionMode && frontmatter.permissionMode.trim()) {
|
|
2980
|
+
cleanedFrontmatter["permissionMode"] = frontmatter.permissionMode;
|
|
2981
|
+
}
|
|
2982
|
+
if (frontmatter.skills && frontmatter.skills.trim()) {
|
|
2983
|
+
cleanedFrontmatter["skills"] = frontmatter.skills;
|
|
2984
|
+
}
|
|
2985
|
+
const content = generateSubagentFile(cleanedFrontmatter, markdown);
|
|
2986
|
+
await fs.promises.writeFile(filePath, content, "utf-8");
|
|
2987
|
+
return { success: true };
|
|
2988
|
+
} catch (error) {
|
|
2989
|
+
console.error("Error writing subagent file:", error);
|
|
2990
|
+
throw new Error(error.message || "Failed to write subagent file");
|
|
2991
|
+
}
|
|
2992
|
+
});
|
|
2993
|
+
electron.ipcMain.handle("rename-subagent-file", async (_, projectPath2, oldFileName, newFileName) => {
|
|
2994
|
+
try {
|
|
2995
|
+
const subagentPath = await ensureSubagentFolder(projectPath2);
|
|
2996
|
+
const oldFilePath = path.join(subagentPath, oldFileName);
|
|
2997
|
+
const newFilePath = path.join(subagentPath, newFileName);
|
|
2998
|
+
await fs.promises.rename(oldFilePath, newFilePath);
|
|
2999
|
+
return { success: true };
|
|
3000
|
+
} catch (error) {
|
|
3001
|
+
console.error("Error renaming subagent file:", error);
|
|
3002
|
+
throw new Error(error.message || "Failed to rename subagent file");
|
|
3003
|
+
}
|
|
3004
|
+
});
|
|
3005
|
+
electron.ipcMain.handle("delete-subagent-file", async (_, projectPath2, fileName) => {
|
|
3006
|
+
try {
|
|
3007
|
+
const subagentPath = await ensureSubagentFolder(projectPath2);
|
|
3008
|
+
const filePath = path.join(subagentPath, fileName);
|
|
3009
|
+
await fs.promises.unlink(filePath);
|
|
3010
|
+
return { success: true };
|
|
3011
|
+
} catch (error) {
|
|
3012
|
+
console.error("Error deleting subagent file:", error);
|
|
3013
|
+
throw new Error(error.message || "Failed to delete subagent file");
|
|
3014
|
+
}
|
|
3015
|
+
});
|
|
3016
|
+
electron.ipcMain.handle("scan-agents-folder", async (_, projectPath2) => {
|
|
3017
|
+
try {
|
|
3018
|
+
const agentsFolder = path.join(projectPath2, ".claude", "agents");
|
|
3019
|
+
try {
|
|
3020
|
+
await fs.promises.access(agentsFolder);
|
|
3021
|
+
} catch {
|
|
3022
|
+
return { agents: [] };
|
|
3023
|
+
}
|
|
3024
|
+
const files = await fs.promises.readdir(agentsFolder);
|
|
3025
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3026
|
+
return { agents: mdFiles };
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
console.error("Error scanning agents folder:", error);
|
|
3029
|
+
throw new Error("Failed to scan agents folder");
|
|
3030
|
+
}
|
|
3031
|
+
});
|
|
3032
|
+
const ensureCommandFolder = async (projectPath2) => {
|
|
3033
|
+
const commandPath = path.join(projectPath2, ".claude", "commands");
|
|
3034
|
+
try {
|
|
3035
|
+
await fs.promises.mkdir(commandPath, { recursive: true });
|
|
3036
|
+
return commandPath;
|
|
3037
|
+
} catch (error) {
|
|
3038
|
+
console.error("Error creating command folder:", error);
|
|
3039
|
+
throw error;
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
const parseCommandFile = (content) => {
|
|
3043
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
3044
|
+
const match = content.match(frontmatterRegex);
|
|
3045
|
+
if (match) {
|
|
3046
|
+
try {
|
|
3047
|
+
const frontmatter = yaml.load(match[1]);
|
|
3048
|
+
const markdown = match[2].trim();
|
|
3049
|
+
return { frontmatter, markdown };
|
|
3050
|
+
} catch (error) {
|
|
3051
|
+
console.error("Error parsing YAML frontmatter:", error);
|
|
3052
|
+
throw new Error("Invalid YAML frontmatter");
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
return { frontmatter: {}, markdown: content.trim() };
|
|
3056
|
+
};
|
|
3057
|
+
const generateCommandFile = (frontmatter, markdown) => {
|
|
3058
|
+
const yamlString = yaml.dump(frontmatter, { lineWidth: -1, noRefs: true });
|
|
3059
|
+
return `---
|
|
3060
|
+
${yamlString}---
|
|
3061
|
+
|
|
3062
|
+
${markdown}`;
|
|
3063
|
+
};
|
|
3064
|
+
electron.ipcMain.handle("list-command-files", async (_, projectPath2) => {
|
|
3065
|
+
try {
|
|
3066
|
+
const commandPath = await ensureCommandFolder(projectPath2);
|
|
3067
|
+
const allFiles = await fs.promises.readdir(commandPath);
|
|
3068
|
+
const mdFiles = allFiles.filter((f) => f.endsWith(".md"));
|
|
3069
|
+
const fileInfos = await Promise.all(
|
|
3070
|
+
mdFiles.map(async (fileName) => {
|
|
3071
|
+
const filePath = path.join(commandPath, fileName);
|
|
3072
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
3073
|
+
const stats = await fs.promises.stat(filePath);
|
|
3074
|
+
const { frontmatter } = parseCommandFile(content);
|
|
3075
|
+
return {
|
|
3076
|
+
fileName,
|
|
3077
|
+
path: filePath,
|
|
3078
|
+
name: frontmatter.name || fileName.replace(".md", ""),
|
|
3079
|
+
description: frontmatter.description || "",
|
|
3080
|
+
created: stats.birthtime.toISOString(),
|
|
3081
|
+
modified: stats.mtime.toISOString()
|
|
3082
|
+
};
|
|
3083
|
+
})
|
|
3084
|
+
);
|
|
3085
|
+
fileInfos.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
3086
|
+
return fileInfos;
|
|
3087
|
+
} catch (error) {
|
|
3088
|
+
console.error("Error listing command files:", error);
|
|
3089
|
+
return [];
|
|
3090
|
+
}
|
|
3091
|
+
});
|
|
3092
|
+
electron.ipcMain.handle("read-command-file", async (_, projectPath2, fileName) => {
|
|
3093
|
+
try {
|
|
3094
|
+
const commandPath = await ensureCommandFolder(projectPath2);
|
|
3095
|
+
const filePath = path.join(commandPath, fileName);
|
|
3096
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
3097
|
+
const { frontmatter, markdown } = parseCommandFile(content);
|
|
3098
|
+
return {
|
|
3099
|
+
success: true,
|
|
3100
|
+
fileName,
|
|
3101
|
+
frontmatter: {
|
|
3102
|
+
name: frontmatter.name || "",
|
|
3103
|
+
description: frontmatter.description || "",
|
|
3104
|
+
// Command configuration fields (using kebab-case from YAML)
|
|
3105
|
+
allowedTools: frontmatter["allowed-tools"] || "",
|
|
3106
|
+
argumentHint: frontmatter["argument-hint"] || "",
|
|
3107
|
+
model: frontmatter.model || ""
|
|
3108
|
+
},
|
|
3109
|
+
markdown
|
|
3110
|
+
};
|
|
3111
|
+
} catch (error) {
|
|
3112
|
+
if (error.code !== "ENOENT") {
|
|
3113
|
+
console.error("Error reading command file:", error);
|
|
3114
|
+
}
|
|
3115
|
+
throw new Error(error.message || "Failed to read command file");
|
|
3116
|
+
}
|
|
3117
|
+
});
|
|
3118
|
+
electron.ipcMain.handle("write-command-file", async (_, projectPath2, fileName, frontmatter, markdown) => {
|
|
3119
|
+
try {
|
|
3120
|
+
const commandPath = await ensureCommandFolder(projectPath2);
|
|
3121
|
+
const filePath = path.join(commandPath, fileName);
|
|
3122
|
+
const cleanedFrontmatter = {};
|
|
3123
|
+
if (frontmatter.name) cleanedFrontmatter.name = frontmatter.name;
|
|
3124
|
+
if (frontmatter.description) cleanedFrontmatter.description = frontmatter.description;
|
|
3125
|
+
if (frontmatter.allowedTools) cleanedFrontmatter["allowed-tools"] = frontmatter.allowedTools;
|
|
3126
|
+
if (frontmatter.argumentHint) cleanedFrontmatter["argument-hint"] = frontmatter.argumentHint;
|
|
3127
|
+
if (frontmatter.model) cleanedFrontmatter.model = frontmatter.model;
|
|
3128
|
+
const content = generateCommandFile(cleanedFrontmatter, markdown);
|
|
3129
|
+
await fs.promises.writeFile(filePath, content, "utf-8");
|
|
3130
|
+
return { success: true };
|
|
3131
|
+
} catch (error) {
|
|
3132
|
+
console.error("Error writing command file:", error);
|
|
3133
|
+
throw new Error(error.message || "Failed to write command file");
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
electron.ipcMain.handle("rename-command-file", async (_, projectPath2, oldFileName, newFileName) => {
|
|
3137
|
+
try {
|
|
3138
|
+
const commandPath = await ensureCommandFolder(projectPath2);
|
|
3139
|
+
const oldFilePath = path.join(commandPath, oldFileName);
|
|
3140
|
+
const newFilePath = path.join(commandPath, newFileName);
|
|
3141
|
+
await fs.promises.rename(oldFilePath, newFilePath);
|
|
3142
|
+
return { success: true };
|
|
3143
|
+
} catch (error) {
|
|
3144
|
+
console.error("Error renaming command file:", error);
|
|
3145
|
+
throw new Error(error.message || "Failed to rename command file");
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
3148
|
+
electron.ipcMain.handle("delete-command-file", async (_, projectPath2, fileName) => {
|
|
3149
|
+
try {
|
|
3150
|
+
const commandPath = await ensureCommandFolder(projectPath2);
|
|
3151
|
+
const filePath = path.join(commandPath, fileName);
|
|
3152
|
+
await fs.promises.unlink(filePath);
|
|
3153
|
+
return { success: true };
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
console.error("Error deleting command file:", error);
|
|
3156
|
+
throw new Error(error.message || "Failed to delete command file");
|
|
3157
|
+
}
|
|
3158
|
+
});
|
|
3159
|
+
electron.ipcMain.handle("scan-commands-folder", async (_, projectPath2) => {
|
|
3160
|
+
try {
|
|
3161
|
+
const commandsFolder = path.join(projectPath2, ".claude", "commands");
|
|
3162
|
+
try {
|
|
3163
|
+
await fs.promises.access(commandsFolder);
|
|
3164
|
+
} catch {
|
|
3165
|
+
return { commands: [] };
|
|
3166
|
+
}
|
|
3167
|
+
const files = await fs.promises.readdir(commandsFolder);
|
|
3168
|
+
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
3169
|
+
return { commands: mdFiles };
|
|
3170
|
+
} catch (error) {
|
|
3171
|
+
console.error("Error scanning commands folder:", error);
|
|
3172
|
+
throw new Error("Failed to scan commands folder");
|
|
3173
|
+
}
|
|
3174
|
+
});
|
|
3175
|
+
const ensureHooksFolder = async (projectPath2) => {
|
|
3176
|
+
const hooksPath = path.join(projectPath2, ".claude", "hooks");
|
|
3177
|
+
try {
|
|
3178
|
+
await fs.promises.mkdir(hooksPath, { recursive: true });
|
|
3179
|
+
return hooksPath;
|
|
3180
|
+
} catch (error) {
|
|
3181
|
+
console.error("Error creating hooks folder:", error);
|
|
3182
|
+
throw error;
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
3185
|
+
electron.ipcMain.handle("list-hook-files", async (_, projectPath2) => {
|
|
3186
|
+
try {
|
|
3187
|
+
const hooksPath = await ensureHooksFolder(projectPath2);
|
|
3188
|
+
const allFiles = await fs.promises.readdir(hooksPath);
|
|
3189
|
+
const shFiles = allFiles.filter((f) => f.endsWith(".sh"));
|
|
3190
|
+
const fileInfos = await Promise.all(
|
|
3191
|
+
shFiles.map(async (fileName) => {
|
|
3192
|
+
const filePath = path.join(hooksPath, fileName);
|
|
3193
|
+
const stats = await fs.promises.stat(filePath);
|
|
3194
|
+
return {
|
|
3195
|
+
fileName,
|
|
3196
|
+
path: filePath,
|
|
3197
|
+
created: stats.birthtime.toISOString(),
|
|
3198
|
+
modified: stats.mtime.toISOString()
|
|
3199
|
+
};
|
|
3200
|
+
})
|
|
3201
|
+
);
|
|
3202
|
+
return fileInfos.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
3203
|
+
} catch (error) {
|
|
3204
|
+
console.error("Error listing hook files:", error);
|
|
3205
|
+
return [];
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3208
|
+
electron.ipcMain.handle("read-hook-file", async (_, projectPath2, fileName) => {
|
|
3209
|
+
try {
|
|
3210
|
+
const hooksPath = await ensureHooksFolder(projectPath2);
|
|
3211
|
+
const filePath = path.join(hooksPath, fileName);
|
|
3212
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
3213
|
+
return {
|
|
3214
|
+
success: true,
|
|
3215
|
+
fileName,
|
|
3216
|
+
content
|
|
3217
|
+
};
|
|
3218
|
+
} catch (error) {
|
|
3219
|
+
if (error.code !== "ENOENT") {
|
|
3220
|
+
console.error("Error reading hook file:", error);
|
|
3221
|
+
}
|
|
3222
|
+
throw new Error(error.message || "Failed to read hook file");
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
electron.ipcMain.handle("write-hook-file", async (_, projectPath2, fileName, content) => {
|
|
3226
|
+
try {
|
|
3227
|
+
const hooksPath = await ensureHooksFolder(projectPath2);
|
|
3228
|
+
const filePath = path.join(hooksPath, fileName);
|
|
3229
|
+
await fs.promises.writeFile(filePath, content, "utf-8");
|
|
3230
|
+
if (process.platform !== "win32") {
|
|
3231
|
+
await fs.promises.chmod(filePath, 493);
|
|
3232
|
+
}
|
|
3233
|
+
return { success: true };
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
console.error("Error writing hook file:", error);
|
|
3236
|
+
throw new Error(error.message || "Failed to write hook file");
|
|
3237
|
+
}
|
|
3238
|
+
});
|
|
3239
|
+
electron.ipcMain.handle("rename-hook-file", async (_, projectPath2, oldFileName, newFileName) => {
|
|
3240
|
+
try {
|
|
3241
|
+
const hooksPath = await ensureHooksFolder(projectPath2);
|
|
3242
|
+
const oldFilePath = path.join(hooksPath, oldFileName);
|
|
3243
|
+
const newFilePath = path.join(hooksPath, newFileName);
|
|
3244
|
+
await fs.promises.rename(oldFilePath, newFilePath);
|
|
3245
|
+
return { success: true };
|
|
3246
|
+
} catch (error) {
|
|
3247
|
+
console.error("Error renaming hook file:", error);
|
|
3248
|
+
throw new Error(error.message || "Failed to rename hook file");
|
|
3249
|
+
}
|
|
3250
|
+
});
|
|
3251
|
+
electron.ipcMain.handle("delete-hook-file", async (_, projectPath2, fileName) => {
|
|
3252
|
+
try {
|
|
3253
|
+
const hooksPath = await ensureHooksFolder(projectPath2);
|
|
3254
|
+
const filePath = path.join(hooksPath, fileName);
|
|
3255
|
+
await fs.promises.unlink(filePath);
|
|
3256
|
+
return { success: true };
|
|
3257
|
+
} catch (error) {
|
|
3258
|
+
console.error("Error deleting hook file:", error);
|
|
3259
|
+
throw new Error(error.message || "Failed to delete hook file");
|
|
3260
|
+
}
|
|
3261
|
+
});
|
|
3262
|
+
electron.ipcMain.handle("scan-hooks-folder", async (_, projectPath2) => {
|
|
3263
|
+
try {
|
|
3264
|
+
const hooksFolder = path.join(projectPath2, ".claude", "hooks");
|
|
3265
|
+
try {
|
|
3266
|
+
await fs.promises.access(hooksFolder);
|
|
3267
|
+
} catch {
|
|
3268
|
+
return { hooks: [] };
|
|
3269
|
+
}
|
|
3270
|
+
const files = await fs.promises.readdir(hooksFolder);
|
|
3271
|
+
const shFiles = files.filter((f) => f.endsWith(".sh"));
|
|
3272
|
+
return { hooks: shFiles };
|
|
3273
|
+
} catch (error) {
|
|
3274
|
+
console.error("Error scanning hooks folder:", error);
|
|
3275
|
+
throw new Error("Failed to scan hooks folder");
|
|
3276
|
+
}
|
|
3277
|
+
});
|
|
3278
|
+
electron.ipcMain.handle("select-project-folder", async () => {
|
|
3279
|
+
try {
|
|
3280
|
+
const result = await electron.dialog.showOpenDialog({
|
|
3281
|
+
properties: ["openDirectory"],
|
|
3282
|
+
title: "Select Project Folder",
|
|
3283
|
+
buttonLabel: "Select Project"
|
|
3284
|
+
});
|
|
3285
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
3286
|
+
return { canceled: true };
|
|
3287
|
+
}
|
|
3288
|
+
return { canceled: false, path: result.filePaths[0] };
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
console.error("Error selecting project folder:", error);
|
|
3291
|
+
throw new Error("Failed to select project folder");
|
|
3292
|
+
}
|
|
3293
|
+
});
|
|
3294
|
+
electron.ipcMain.handle("validate-project", async (_, projectPath2) => {
|
|
3295
|
+
try {
|
|
3296
|
+
const workflowPath = path.join(projectPath2, ".claude", "workflow.json");
|
|
3297
|
+
await fs.promises.access(workflowPath);
|
|
3298
|
+
return { valid: true };
|
|
3299
|
+
} catch {
|
|
3300
|
+
return { valid: false };
|
|
3301
|
+
}
|
|
3302
|
+
});
|
|
3303
|
+
electron.ipcMain.handle("check-claude-folder", async (_, projectPath2) => {
|
|
3304
|
+
try {
|
|
3305
|
+
console.log("[Main] check-claude-folder called with:", projectPath2);
|
|
3306
|
+
const claudePath = path.join(projectPath2, ".claude");
|
|
3307
|
+
const workflowPath = path.join(claudePath, "workflow.json");
|
|
3308
|
+
console.log("[Main] Checking paths:", { claudePath, workflowPath });
|
|
3309
|
+
let claudeFolderExists = false;
|
|
3310
|
+
try {
|
|
3311
|
+
const stats = await fs.promises.stat(claudePath);
|
|
3312
|
+
claudeFolderExists = stats.isDirectory();
|
|
3313
|
+
console.log("[Main] .claude folder exists:", claudeFolderExists);
|
|
3314
|
+
} catch (err) {
|
|
3315
|
+
claudeFolderExists = false;
|
|
3316
|
+
console.log("[Main] .claude folder does not exist");
|
|
3317
|
+
}
|
|
3318
|
+
let workflowExists = false;
|
|
3319
|
+
try {
|
|
3320
|
+
await fs.promises.access(workflowPath);
|
|
3321
|
+
workflowExists = true;
|
|
3322
|
+
console.log("[Main] workflow.json exists:", workflowExists);
|
|
3323
|
+
} catch (err) {
|
|
3324
|
+
workflowExists = false;
|
|
3325
|
+
console.log("[Main] workflow.json does not exist");
|
|
3326
|
+
}
|
|
3327
|
+
const result = {
|
|
3328
|
+
success: true,
|
|
3329
|
+
claudeFolderExists,
|
|
3330
|
+
workflowExists
|
|
3331
|
+
};
|
|
3332
|
+
console.log("[Main] Returning result:", result);
|
|
3333
|
+
return result;
|
|
3334
|
+
} catch (error) {
|
|
3335
|
+
console.error("[Main] Error checking claude folder:", error);
|
|
3336
|
+
return {
|
|
3337
|
+
success: false,
|
|
3338
|
+
claudeFolderExists: false,
|
|
3339
|
+
workflowExists: false,
|
|
3340
|
+
error: error.message
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
});
|
|
3344
|
+
electron.ipcMain.handle("list-projects", async (_, workspacePath) => {
|
|
3345
|
+
try {
|
|
3346
|
+
const entries = await fs.promises.readdir(workspacePath, { withFileTypes: true });
|
|
3347
|
+
const projects = [];
|
|
3348
|
+
for (const entry of entries) {
|
|
3349
|
+
if (entry.isDirectory()) {
|
|
3350
|
+
const projectPath2 = path.join(workspacePath, entry.name);
|
|
3351
|
+
const claudeFolderPath = path.join(projectPath2, ".claude");
|
|
3352
|
+
const workflowPath = path.join(claudeFolderPath, "workflow.json");
|
|
3353
|
+
try {
|
|
3354
|
+
await fs.promises.access(workflowPath);
|
|
3355
|
+
let metadata = { name: entry.name, created: null, modified: null, iconImage: null, systemPrompt: null };
|
|
3356
|
+
try {
|
|
3357
|
+
const workflowContent = await fs.promises.readFile(workflowPath, "utf-8");
|
|
3358
|
+
const workflow = JSON.parse(workflowContent);
|
|
3359
|
+
let iconImage = null;
|
|
3360
|
+
let systemPrompt = null;
|
|
3361
|
+
let agentName = workflow.name || entry.name;
|
|
3362
|
+
if (workflow.visual && workflow.visual.nodes) {
|
|
3363
|
+
const mainCard = workflow.visual.nodes.find((node) => node.id === "main-card");
|
|
3364
|
+
if (mainCard && mainCard.data) {
|
|
3365
|
+
iconImage = mainCard.data.iconImage || null;
|
|
3366
|
+
systemPrompt = mainCard.data.systemPrompt || mainCard.data.description || null;
|
|
3367
|
+
agentName = mainCard.data.name || workflow.name || entry.name;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
metadata = {
|
|
3371
|
+
name: agentName,
|
|
3372
|
+
created: workflow.created,
|
|
3373
|
+
modified: workflow.modified,
|
|
3374
|
+
iconImage,
|
|
3375
|
+
systemPrompt
|
|
3376
|
+
};
|
|
3377
|
+
} catch {
|
|
3378
|
+
}
|
|
3379
|
+
projects.push({
|
|
3380
|
+
folderName: entry.name,
|
|
3381
|
+
path: projectPath2,
|
|
3382
|
+
...metadata
|
|
3383
|
+
});
|
|
3384
|
+
} catch {
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
return projects;
|
|
3389
|
+
} catch (error) {
|
|
3390
|
+
console.error("Error listing projects:", error);
|
|
3391
|
+
return [];
|
|
3392
|
+
}
|
|
3393
|
+
});
|
|
3394
|
+
electron.ipcMain.handle("create-project-at", async (_, targetPath, projectName) => {
|
|
3395
|
+
try {
|
|
3396
|
+
const projectPath2 = path.join(targetPath, projectName);
|
|
3397
|
+
const claudeFolder = path.join(projectPath2, ".claude");
|
|
3398
|
+
try {
|
|
3399
|
+
await fs.promises.access(projectPath2);
|
|
3400
|
+
throw new Error("Project folder already exists");
|
|
3401
|
+
} catch (err) {
|
|
3402
|
+
if (err.code !== "ENOENT" && err.message !== "Project folder already exists") {
|
|
3403
|
+
throw err;
|
|
3404
|
+
}
|
|
3405
|
+
if (err.message === "Project folder already exists") {
|
|
3406
|
+
throw err;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
await fs.promises.mkdir(claudeFolder, { recursive: true });
|
|
3410
|
+
await fs.promises.mkdir(path.join(claudeFolder, "agents"), { recursive: true });
|
|
3411
|
+
await fs.promises.mkdir(path.join(claudeFolder, "skills"), { recursive: true });
|
|
3412
|
+
await fs.promises.mkdir(path.join(claudeFolder, "memory"), { recursive: true });
|
|
3413
|
+
const initialWorkflow = {
|
|
3414
|
+
version: "1.0",
|
|
3415
|
+
name: projectName,
|
|
3416
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3417
|
+
modified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3418
|
+
visual: {
|
|
3419
|
+
nodes: [],
|
|
3420
|
+
edges: []
|
|
3421
|
+
},
|
|
3422
|
+
connections: {
|
|
3423
|
+
interface: [],
|
|
3424
|
+
memory: [],
|
|
3425
|
+
skills: [],
|
|
3426
|
+
mcp: [],
|
|
3427
|
+
subagents: []
|
|
3428
|
+
}
|
|
3429
|
+
};
|
|
3430
|
+
await fs.promises.writeFile(
|
|
3431
|
+
path.join(claudeFolder, "workflow.json"),
|
|
3432
|
+
JSON.stringify(initialWorkflow, null, 2),
|
|
3433
|
+
"utf-8"
|
|
3434
|
+
);
|
|
3435
|
+
return { success: true, path: projectPath2 };
|
|
3436
|
+
} catch (error) {
|
|
3437
|
+
console.error("Error creating project:", error);
|
|
3438
|
+
throw new Error(error.message || "Failed to create project");
|
|
3439
|
+
}
|
|
3440
|
+
});
|
|
3441
|
+
electron.ipcMain.handle("save-project", async (_, projectPath2, workflowData) => {
|
|
3442
|
+
try {
|
|
3443
|
+
const claudeFolder = path.join(projectPath2, ".claude");
|
|
3444
|
+
const workflowPath = path.join(claudeFolder, "workflow.json");
|
|
3445
|
+
await fs.promises.mkdir(claudeFolder, { recursive: true });
|
|
3446
|
+
const updatedWorkflow = {
|
|
3447
|
+
...workflowData,
|
|
3448
|
+
modified: (/* @__PURE__ */ new Date()).toISOString()
|
|
3449
|
+
};
|
|
3450
|
+
await fs.promises.writeFile(workflowPath, JSON.stringify(updatedWorkflow, null, 2), "utf-8");
|
|
3451
|
+
const mainCard = updatedWorkflow.visual?.nodes?.find((node) => node.id === "main-card");
|
|
3452
|
+
if (mainCard && mainCard.data) {
|
|
3453
|
+
const claudeMdPath = path.join(claudeFolder, "CLAUDE.md");
|
|
3454
|
+
const systemPrompt = mainCard.data.systemPrompt || "";
|
|
3455
|
+
await fs.promises.writeFile(claudeMdPath, systemPrompt, "utf-8");
|
|
3456
|
+
}
|
|
3457
|
+
return { success: true };
|
|
3458
|
+
} catch (error) {
|
|
3459
|
+
console.error("Error saving project:", error);
|
|
3460
|
+
throw new Error(error.message || "Failed to save project");
|
|
3461
|
+
}
|
|
3462
|
+
});
|
|
3463
|
+
electron.ipcMain.handle("load-project", async (_, projectPath2) => {
|
|
3464
|
+
try {
|
|
3465
|
+
const workflowPath = path.join(projectPath2, ".claude", "workflow.json");
|
|
3466
|
+
const content = await fs.promises.readFile(workflowPath, "utf-8");
|
|
3467
|
+
const workflow = JSON.parse(content);
|
|
3468
|
+
return workflow;
|
|
3469
|
+
} catch (error) {
|
|
3470
|
+
console.error("Error loading project:", error);
|
|
3471
|
+
throw new Error("Failed to load project");
|
|
3472
|
+
}
|
|
3473
|
+
});
|
|
3474
|
+
electron.ipcMain.handle("delete-project", async (_, projectPath2) => {
|
|
3475
|
+
try {
|
|
3476
|
+
await fs.promises.rm(projectPath2, { recursive: true, force: true });
|
|
3477
|
+
return { success: true };
|
|
3478
|
+
} catch (error) {
|
|
3479
|
+
console.error("Error deleting project:", error);
|
|
3480
|
+
throw new Error("Failed to delete project");
|
|
3481
|
+
}
|
|
3482
|
+
});
|
|
3483
|
+
electron.ipcMain.handle("select-export-location", async () => {
|
|
3484
|
+
try {
|
|
3485
|
+
const result = await electron.dialog.showOpenDialog({
|
|
3486
|
+
properties: ["openDirectory", "createDirectory"],
|
|
3487
|
+
title: "Select Export Location",
|
|
3488
|
+
buttonLabel: "Export Here"
|
|
3489
|
+
});
|
|
3490
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
3491
|
+
return { canceled: true };
|
|
3492
|
+
}
|
|
3493
|
+
return { canceled: false, path: result.filePaths[0] };
|
|
3494
|
+
} catch (error) {
|
|
3495
|
+
console.error("Error selecting export location:", error);
|
|
3496
|
+
throw new Error("Failed to select export location");
|
|
3497
|
+
}
|
|
3498
|
+
});
|
|
3499
|
+
electron.ipcMain.handle(
|
|
3500
|
+
"export-claude-folder",
|
|
3501
|
+
async (_, exportPath, projectName, exportData) => {
|
|
3502
|
+
try {
|
|
3503
|
+
const projectFolder = path.join(exportPath, projectName);
|
|
3504
|
+
const claudeFolder = path.join(projectFolder, ".claude");
|
|
3505
|
+
await fs.promises.mkdir(claudeFolder, { recursive: true });
|
|
3506
|
+
await fs.promises.mkdir(path.join(claudeFolder, "agents"), { recursive: true });
|
|
3507
|
+
await fs.promises.mkdir(path.join(claudeFolder, "skills"), { recursive: true });
|
|
3508
|
+
await fs.promises.mkdir(path.join(claudeFolder, "memory"), { recursive: true });
|
|
3509
|
+
await fs.promises.writeFile(
|
|
3510
|
+
path.join(claudeFolder, "settings.json"),
|
|
3511
|
+
JSON.stringify(exportData.settings, null, 2),
|
|
3512
|
+
"utf-8"
|
|
3513
|
+
);
|
|
3514
|
+
if (exportData.settingsLocal) {
|
|
3515
|
+
await fs.promises.writeFile(
|
|
3516
|
+
path.join(claudeFolder, "settings.local.json"),
|
|
3517
|
+
JSON.stringify(exportData.settingsLocal, null, 2),
|
|
3518
|
+
"utf-8"
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
if (exportData.mcpJson) {
|
|
3522
|
+
await fs.promises.writeFile(
|
|
3523
|
+
path.join(projectFolder, ".mcp.json"),
|
|
3524
|
+
JSON.stringify(exportData.mcpJson, null, 2),
|
|
3525
|
+
"utf-8"
|
|
3526
|
+
);
|
|
3527
|
+
}
|
|
3528
|
+
for (const [agentId, content] of Object.entries(exportData.agents)) {
|
|
3529
|
+
const fileName = `${agentId}.md`;
|
|
3530
|
+
await fs.promises.writeFile(path.join(claudeFolder, "agents", fileName), content, "utf-8");
|
|
3531
|
+
}
|
|
3532
|
+
for (const [fileName, content] of Object.entries(exportData.skills)) {
|
|
3533
|
+
await fs.promises.writeFile(path.join(claudeFolder, "skills", fileName), content, "utf-8");
|
|
3534
|
+
}
|
|
3535
|
+
for (const [fileName, content] of Object.entries(exportData.memory)) {
|
|
3536
|
+
await fs.promises.writeFile(path.join(claudeFolder, "memory", fileName), content, "utf-8");
|
|
3537
|
+
}
|
|
3538
|
+
return { success: true, path: projectFolder };
|
|
3539
|
+
} catch (error) {
|
|
3540
|
+
console.error("Error exporting Claude folder:", error);
|
|
3541
|
+
throw new Error(error.message || "Failed to export project");
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
);
|
|
3545
|
+
electron.ipcMain.handle("select-import-folder", async () => {
|
|
3546
|
+
try {
|
|
3547
|
+
const result = await electron.dialog.showOpenDialog({
|
|
3548
|
+
properties: ["openDirectory"],
|
|
3549
|
+
title: "Select .claude Folder to Import",
|
|
3550
|
+
buttonLabel: "Import"
|
|
3551
|
+
});
|
|
3552
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
3553
|
+
return { canceled: true };
|
|
3554
|
+
}
|
|
3555
|
+
return { canceled: false, path: result.filePaths[0] };
|
|
3556
|
+
} catch (error) {
|
|
3557
|
+
console.error("Error selecting import folder:", error);
|
|
3558
|
+
throw new Error("Failed to select import folder");
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
electron.ipcMain.handle("import-claude-folder", async (_, claudeFolderPath) => {
|
|
3562
|
+
try {
|
|
3563
|
+
const settingsPath = path.join(claudeFolderPath, "settings.json");
|
|
3564
|
+
const settingsContent = await fs.promises.readFile(settingsPath, "utf-8");
|
|
3565
|
+
const settings = JSON.parse(settingsContent);
|
|
3566
|
+
let settingsLocal = void 0;
|
|
3567
|
+
try {
|
|
3568
|
+
const settingsLocalPath = path.join(claudeFolderPath, "settings.local.json");
|
|
3569
|
+
const settingsLocalContent = await fs.promises.readFile(settingsLocalPath, "utf-8");
|
|
3570
|
+
settingsLocal = JSON.parse(settingsLocalContent);
|
|
3571
|
+
} catch {
|
|
3572
|
+
}
|
|
3573
|
+
let mcpJson = void 0;
|
|
3574
|
+
try {
|
|
3575
|
+
const projectRoot = path.join(claudeFolderPath, "..");
|
|
3576
|
+
const mcpJsonPath = path.join(projectRoot, ".mcp.json");
|
|
3577
|
+
const mcpJsonContent = await fs.promises.readFile(mcpJsonPath, "utf-8");
|
|
3578
|
+
mcpJson = JSON.parse(mcpJsonContent);
|
|
3579
|
+
} catch {
|
|
3580
|
+
}
|
|
3581
|
+
const agentsFolder = path.join(claudeFolderPath, "agents");
|
|
3582
|
+
const agentContents = {};
|
|
3583
|
+
try {
|
|
3584
|
+
const agentFiles = await fs.promises.readdir(agentsFolder);
|
|
3585
|
+
for (const file of agentFiles) {
|
|
3586
|
+
if (file.endsWith(".md")) {
|
|
3587
|
+
const content = await fs.promises.readFile(path.join(agentsFolder, file), "utf-8");
|
|
3588
|
+
agentContents[file] = content;
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
} catch {
|
|
3592
|
+
}
|
|
3593
|
+
const skillsFolder = path.join(claudeFolderPath, "skills");
|
|
3594
|
+
const skillContents = {};
|
|
3595
|
+
try {
|
|
3596
|
+
const skillFiles = await fs.promises.readdir(skillsFolder);
|
|
3597
|
+
for (const file of skillFiles) {
|
|
3598
|
+
const content = await fs.promises.readFile(path.join(skillsFolder, file), "utf-8");
|
|
3599
|
+
skillContents[file] = content;
|
|
3600
|
+
}
|
|
3601
|
+
} catch {
|
|
3602
|
+
}
|
|
3603
|
+
const memoryFolder = path.join(claudeFolderPath, "memory");
|
|
3604
|
+
const memoryContents = {};
|
|
3605
|
+
try {
|
|
3606
|
+
const memoryFiles = await fs.promises.readdir(memoryFolder);
|
|
3607
|
+
for (const file of memoryFiles) {
|
|
3608
|
+
const content = await fs.promises.readFile(path.join(memoryFolder, file), "utf-8");
|
|
3609
|
+
memoryContents[file] = content;
|
|
3610
|
+
}
|
|
3611
|
+
} catch {
|
|
3612
|
+
}
|
|
3613
|
+
return {
|
|
3614
|
+
success: true,
|
|
3615
|
+
data: {
|
|
3616
|
+
settings,
|
|
3617
|
+
settingsLocal,
|
|
3618
|
+
mcpJson,
|
|
3619
|
+
agentContents,
|
|
3620
|
+
skillContents,
|
|
3621
|
+
memoryContents
|
|
3622
|
+
}
|
|
3623
|
+
};
|
|
3624
|
+
} catch (error) {
|
|
3625
|
+
console.error("Error importing Claude folder:", error);
|
|
3626
|
+
throw new Error(error.message || "Failed to import project");
|
|
3627
|
+
}
|
|
3628
|
+
});
|
|
3629
|
+
electron.ipcMain.handle("load-settings-local", async (_, projectPath2) => {
|
|
3630
|
+
try {
|
|
3631
|
+
const claudeFolderPath = path.join(projectPath2, ".claude");
|
|
3632
|
+
const settingsLocalPath = path.join(claudeFolderPath, "settings.local.json");
|
|
3633
|
+
try {
|
|
3634
|
+
await fs.promises.access(settingsLocalPath);
|
|
3635
|
+
} catch {
|
|
3636
|
+
return { success: true, settings: {} };
|
|
3637
|
+
}
|
|
3638
|
+
const content = await fs.promises.readFile(settingsLocalPath, "utf-8");
|
|
3639
|
+
const settings = JSON.parse(content);
|
|
3640
|
+
return {
|
|
3641
|
+
success: true,
|
|
3642
|
+
settings
|
|
3643
|
+
};
|
|
3644
|
+
} catch (error) {
|
|
3645
|
+
console.error("Error loading settings.local.json:", error);
|
|
3646
|
+
return { success: false, settings: {} };
|
|
3647
|
+
}
|
|
3648
|
+
});
|
|
3649
|
+
electron.ipcMain.handle("save-settings-local", async (_, projectPath2, settings) => {
|
|
3650
|
+
try {
|
|
3651
|
+
const claudeFolderPath = path.join(projectPath2, ".claude");
|
|
3652
|
+
const settingsLocalPath = path.join(claudeFolderPath, "settings.local.json");
|
|
3653
|
+
await fs.promises.mkdir(claudeFolderPath, { recursive: true });
|
|
3654
|
+
await fs.promises.writeFile(settingsLocalPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
3655
|
+
return { success: true };
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
console.error("Error saving settings.local.json:", error);
|
|
3658
|
+
return { success: false };
|
|
3659
|
+
}
|
|
3660
|
+
});
|
|
3661
|
+
electron.ipcMain.handle("load-mcp-json", async (_, projectPath2) => {
|
|
3662
|
+
try {
|
|
3663
|
+
const mcpJsonPath = path.join(projectPath2, ".mcp.json");
|
|
3664
|
+
try {
|
|
3665
|
+
await fs.promises.access(mcpJsonPath);
|
|
3666
|
+
} catch {
|
|
3667
|
+
return { success: true, mcpJson: null };
|
|
3668
|
+
}
|
|
3669
|
+
const content = await fs.promises.readFile(mcpJsonPath, "utf-8");
|
|
3670
|
+
const mcpJson = JSON.parse(content);
|
|
3671
|
+
return {
|
|
3672
|
+
success: true,
|
|
3673
|
+
mcpJson
|
|
3674
|
+
};
|
|
3675
|
+
} catch (error) {
|
|
3676
|
+
console.error("Error loading .mcp.json:", error);
|
|
3677
|
+
return { success: false, mcpJson: null };
|
|
3678
|
+
}
|
|
3679
|
+
});
|
|
3680
|
+
electron.ipcMain.handle("save-mcp-json", async (_, projectPath2, mcpJson) => {
|
|
3681
|
+
try {
|
|
3682
|
+
const mcpJsonPath = path.join(projectPath2, ".mcp.json");
|
|
3683
|
+
await fs.promises.writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2), "utf-8");
|
|
3684
|
+
return { success: true };
|
|
3685
|
+
} catch (error) {
|
|
3686
|
+
console.error("Error saving .mcp.json:", error);
|
|
3687
|
+
return { success: false };
|
|
3688
|
+
}
|
|
3689
|
+
});
|
|
3690
|
+
const getGlobalIconsPath = async () => {
|
|
3691
|
+
const iconsPath = path.join(electron.app.getPath("userData"), "user-icons");
|
|
3692
|
+
await fs.promises.mkdir(iconsPath, { recursive: true });
|
|
3693
|
+
return iconsPath;
|
|
3694
|
+
};
|
|
3695
|
+
electron.ipcMain.handle("icons:list", async () => {
|
|
3696
|
+
try {
|
|
3697
|
+
const iconsPath = await getGlobalIconsPath();
|
|
3698
|
+
const files = await fs.promises.readdir(iconsPath, { withFileTypes: true });
|
|
3699
|
+
const iconFiles = await Promise.all(
|
|
3700
|
+
files.filter((f) => f.isFile() && /\.(png|gif|svg)$/i.test(f.name)).map(async (file) => {
|
|
3701
|
+
const filePath = path.join(iconsPath, file.name);
|
|
3702
|
+
const stats = await fs.promises.stat(filePath);
|
|
3703
|
+
const ext = path.extname(file.name).toLowerCase().slice(1);
|
|
3704
|
+
return {
|
|
3705
|
+
id: path.basename(file.name, path.extname(file.name)),
|
|
3706
|
+
name: file.name,
|
|
3707
|
+
path: filePath,
|
|
3708
|
+
size: stats.size,
|
|
3709
|
+
type: ext,
|
|
3710
|
+
dateAdded: stats.birthtime.toISOString()
|
|
3711
|
+
};
|
|
3712
|
+
})
|
|
3713
|
+
);
|
|
3714
|
+
return iconFiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
3715
|
+
} catch (error) {
|
|
3716
|
+
console.error("Error listing user icons:", error);
|
|
3717
|
+
return [];
|
|
3718
|
+
}
|
|
3719
|
+
});
|
|
3720
|
+
electron.ipcMain.handle("icons:add", async (_, sourcePath) => {
|
|
3721
|
+
try {
|
|
3722
|
+
const iconsPath = await getGlobalIconsPath();
|
|
3723
|
+
const fileName = path.basename(sourcePath);
|
|
3724
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
3725
|
+
if (![".png", ".gif", ".svg"].includes(ext)) {
|
|
3726
|
+
throw new Error("Invalid file type. Only PNG, GIF, and SVG are supported.");
|
|
3727
|
+
}
|
|
3728
|
+
const stats = await fs.promises.stat(sourcePath);
|
|
3729
|
+
const MAX_SIZE = 10 * 1024 * 1024;
|
|
3730
|
+
if (stats.size > MAX_SIZE) {
|
|
3731
|
+
throw new Error(`File too large. Maximum size is ${MAX_SIZE / 1024 / 1024}MB.`);
|
|
3732
|
+
}
|
|
3733
|
+
const destPath = path.join(iconsPath, fileName);
|
|
3734
|
+
try {
|
|
3735
|
+
await fs.promises.access(destPath);
|
|
3736
|
+
throw new Error(`Icon "${fileName}" already exists`);
|
|
3737
|
+
} catch (err) {
|
|
3738
|
+
if (err.code !== "ENOENT") throw err;
|
|
3739
|
+
}
|
|
3740
|
+
await fs.promises.copyFile(sourcePath, destPath);
|
|
3741
|
+
return {
|
|
3742
|
+
success: true,
|
|
3743
|
+
icon: {
|
|
3744
|
+
id: path.basename(fileName, ext),
|
|
3745
|
+
name: fileName,
|
|
3746
|
+
path: destPath,
|
|
3747
|
+
size: stats.size,
|
|
3748
|
+
type: ext.slice(1),
|
|
3749
|
+
dateAdded: (/* @__PURE__ */ new Date()).toISOString()
|
|
3750
|
+
}
|
|
3751
|
+
};
|
|
3752
|
+
} catch (error) {
|
|
3753
|
+
console.error("Error adding user icon:", error);
|
|
3754
|
+
throw new Error(error.message || "Failed to add icon");
|
|
3755
|
+
}
|
|
3756
|
+
});
|
|
3757
|
+
electron.ipcMain.handle("icons:delete", async (_, fileName) => {
|
|
3758
|
+
try {
|
|
3759
|
+
const iconsPath = await getGlobalIconsPath();
|
|
3760
|
+
const filePath = path.join(iconsPath, fileName);
|
|
3761
|
+
const resolvedPath = path.resolve(filePath);
|
|
3762
|
+
const resolvedIconsPath = path.resolve(iconsPath);
|
|
3763
|
+
if (!resolvedPath.startsWith(resolvedIconsPath)) {
|
|
3764
|
+
throw new Error("Invalid file path");
|
|
3765
|
+
}
|
|
3766
|
+
await fs.promises.unlink(filePath);
|
|
3767
|
+
return { success: true };
|
|
3768
|
+
} catch (error) {
|
|
3769
|
+
console.error("Error deleting user icon:", error);
|
|
3770
|
+
throw new Error(error.message || "Failed to delete icon");
|
|
3771
|
+
}
|
|
3772
|
+
});
|
|
3773
|
+
electron.ipcMain.handle("icons:select-files", async () => {
|
|
3774
|
+
try {
|
|
3775
|
+
const result = await electron.dialog.showOpenDialog({
|
|
3776
|
+
properties: ["openFile", "multiSelections"],
|
|
3777
|
+
title: "Select Icon Files",
|
|
3778
|
+
buttonLabel: "Add Icons",
|
|
3779
|
+
filters: [
|
|
3780
|
+
{ name: "Images", extensions: ["png", "gif", "svg"] }
|
|
3781
|
+
]
|
|
3782
|
+
});
|
|
3783
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
3784
|
+
return { canceled: true };
|
|
3785
|
+
}
|
|
3786
|
+
return {
|
|
3787
|
+
canceled: false,
|
|
3788
|
+
filePaths: result.filePaths
|
|
3789
|
+
};
|
|
3790
|
+
} catch (error) {
|
|
3791
|
+
console.error("Error selecting icon files:", error);
|
|
3792
|
+
throw new Error("Failed to select files");
|
|
3793
|
+
}
|
|
3794
|
+
});
|
|
3795
|
+
electron.ipcMain.handle("icons:read-file", async (_, filePath) => {
|
|
3796
|
+
try {
|
|
3797
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
3798
|
+
const base64 = buffer.toString("base64");
|
|
3799
|
+
const ext = path.extname(filePath).toLowerCase().slice(1);
|
|
3800
|
+
const mimeType = ext === "svg" ? "image/svg+xml" : `image/${ext}`;
|
|
3801
|
+
return `data:${mimeType};base64,${base64}`;
|
|
3802
|
+
} catch (error) {
|
|
3803
|
+
console.error("Error reading icon file:", error);
|
|
3804
|
+
throw new Error("Failed to read icon file");
|
|
3805
|
+
}
|
|
3806
|
+
});
|
|
3807
|
+
const args = process.argv.slice(1);
|
|
3808
|
+
const { projectPath, openCanvas } = parseCliArguments(args);
|
|
3809
|
+
createWindow();
|
|
3810
|
+
mainWindow?.webContents.once("did-finish-load", () => {
|
|
3811
|
+
if (projectPath && mainWindow) {
|
|
3812
|
+
mainWindow.webContents.send("initial-project-path", projectPath);
|
|
3813
|
+
if (openCanvas) {
|
|
3814
|
+
mainWindow.webContents.send("initial-open-canvas", true);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
const cwd = process.env.CLAUDE_SPACE_CWD || electron.app.getPath("documents");
|
|
3818
|
+
if (mainWindow) {
|
|
3819
|
+
mainWindow.webContents.send("initial-cwd", cwd);
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
electron.app.on("activate", function() {
|
|
3823
|
+
if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
3824
|
+
});
|
|
3825
|
+
});
|
|
3826
|
+
electron.app.on("window-all-closed", () => {
|
|
3827
|
+
if (process.platform !== "darwin") {
|
|
3828
|
+
electron.app.quit();
|
|
3829
|
+
}
|
|
3830
|
+
});
|