claudeck 1.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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
@@ -0,0 +1,294 @@
1
+ import { Router } from "express";
2
+ import { readFile, writeFile } from "fs/promises";
3
+ import { configPath } from "../paths.js";
4
+ import { getAllAgentContext } from "../../db.js";
5
+
6
+ const router = Router();
7
+
8
+ async function readAgents() {
9
+ const data = await readFile(configPath("agents.json"), "utf-8");
10
+ return JSON.parse(data);
11
+ }
12
+
13
+ async function writeAgents(agents) {
14
+ await writeFile(configPath("agents.json"), JSON.stringify(agents, null, 2) + "\n");
15
+ }
16
+
17
+ async function readChains() {
18
+ try {
19
+ const data = await readFile(configPath("agent-chains.json"), "utf-8");
20
+ return JSON.parse(data);
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ async function writeChains(chains) {
27
+ await writeFile(configPath("agent-chains.json"), JSON.stringify(chains, null, 2) + "\n");
28
+ }
29
+
30
+ async function readDags() {
31
+ try {
32
+ const data = await readFile(configPath("agent-dags.json"), "utf-8");
33
+ return JSON.parse(data);
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ async function writeDags(dags) {
40
+ await writeFile(configPath("agent-dags.json"), JSON.stringify(dags, null, 2) + "\n");
41
+ }
42
+
43
+ function slugify(text) {
44
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
45
+ }
46
+
47
+ // ── Agent Context (shared memory) ──
48
+
49
+ router.get("/context/:runId", (req, res) => {
50
+ try {
51
+ const rows = getAllAgentContext(req.params.runId);
52
+ res.json(rows);
53
+ } catch (err) {
54
+ res.status(500).json({ error: err.message });
55
+ }
56
+ });
57
+
58
+ // ── Agent Chains CRUD (must come before /:id to avoid route conflict) ──
59
+
60
+ router.get("/chains", async (req, res) => {
61
+ try {
62
+ res.json(await readChains());
63
+ } catch (err) {
64
+ res.status(500).json({ error: err.message });
65
+ }
66
+ });
67
+
68
+ router.get("/chains/:id", async (req, res) => {
69
+ try {
70
+ const chains = await readChains();
71
+ const chain = chains.find((c) => c.id === req.params.id);
72
+ if (!chain) return res.status(404).json({ error: "Chain not found" });
73
+ res.json(chain);
74
+ } catch (err) {
75
+ res.status(500).json({ error: err.message });
76
+ }
77
+ });
78
+
79
+ router.post("/chains", async (req, res) => {
80
+ try {
81
+ const { title, description, agents: agentIds, contextPassing } = req.body;
82
+ if (!title || !agentIds?.length) {
83
+ return res.status(400).json({ error: "title and agents are required" });
84
+ }
85
+ const chains = await readChains();
86
+ const id = slugify(title);
87
+ if (chains.find((c) => c.id === id)) {
88
+ return res.status(409).json({ error: `Chain "${id}" already exists` });
89
+ }
90
+ const chain = {
91
+ id,
92
+ title,
93
+ description: description || "",
94
+ agents: agentIds,
95
+ contextPassing: contextPassing || "summary",
96
+ };
97
+ chains.push(chain);
98
+ await writeChains(chains);
99
+ res.json(chain);
100
+ } catch (err) {
101
+ res.status(500).json({ error: err.message });
102
+ }
103
+ });
104
+
105
+ router.put("/chains/:id", async (req, res) => {
106
+ try {
107
+ const chains = await readChains();
108
+ const idx = chains.findIndex((c) => c.id === req.params.id);
109
+ if (idx === -1) return res.status(404).json({ error: "Chain not found" });
110
+ const { title, description, agents: agentIds, contextPassing } = req.body;
111
+ if (title !== undefined) chains[idx].title = title;
112
+ if (description !== undefined) chains[idx].description = description;
113
+ if (agentIds !== undefined) chains[idx].agents = agentIds;
114
+ if (contextPassing !== undefined) chains[idx].contextPassing = contextPassing;
115
+ await writeChains(chains);
116
+ res.json(chains[idx]);
117
+ } catch (err) {
118
+ res.status(500).json({ error: err.message });
119
+ }
120
+ });
121
+
122
+ router.delete("/chains/:id", async (req, res) => {
123
+ try {
124
+ const chains = await readChains();
125
+ const idx = chains.findIndex((c) => c.id === req.params.id);
126
+ if (idx === -1) return res.status(404).json({ error: "Chain not found" });
127
+ chains.splice(idx, 1);
128
+ await writeChains(chains);
129
+ res.json({ ok: true });
130
+ } catch (err) {
131
+ res.status(500).json({ error: err.message });
132
+ }
133
+ });
134
+
135
+ // ── Agent DAGs CRUD ──
136
+
137
+ router.get("/dags", async (req, res) => {
138
+ try {
139
+ res.json(await readDags());
140
+ } catch (err) {
141
+ res.status(500).json({ error: err.message });
142
+ }
143
+ });
144
+
145
+ router.get("/dags/:id", async (req, res) => {
146
+ try {
147
+ const dags = await readDags();
148
+ const dag = dags.find((d) => d.id === req.params.id);
149
+ if (!dag) return res.status(404).json({ error: "DAG not found" });
150
+ res.json(dag);
151
+ } catch (err) {
152
+ res.status(500).json({ error: err.message });
153
+ }
154
+ });
155
+
156
+ router.post("/dags", async (req, res) => {
157
+ try {
158
+ const { title, description, nodes, edges } = req.body;
159
+ if (!title || !nodes?.length) {
160
+ return res.status(400).json({ error: "title and nodes are required" });
161
+ }
162
+ const dags = await readDags();
163
+ const id = slugify(title);
164
+ if (dags.find((d) => d.id === id)) {
165
+ return res.status(409).json({ error: `DAG "${id}" already exists` });
166
+ }
167
+ const dag = { id, title, description: description || "", nodes, edges: edges || [] };
168
+ dags.push(dag);
169
+ await writeDags(dags);
170
+ res.json(dag);
171
+ } catch (err) {
172
+ res.status(500).json({ error: err.message });
173
+ }
174
+ });
175
+
176
+ router.put("/dags/:id", async (req, res) => {
177
+ try {
178
+ const dags = await readDags();
179
+ const idx = dags.findIndex((d) => d.id === req.params.id);
180
+ if (idx === -1) return res.status(404).json({ error: "DAG not found" });
181
+ const { title, description, nodes, edges } = req.body;
182
+ if (title !== undefined) dags[idx].title = title;
183
+ if (description !== undefined) dags[idx].description = description;
184
+ if (nodes !== undefined) dags[idx].nodes = nodes;
185
+ if (edges !== undefined) dags[idx].edges = edges;
186
+ await writeDags(dags);
187
+ res.json(dags[idx]);
188
+ } catch (err) {
189
+ res.status(500).json({ error: err.message });
190
+ }
191
+ });
192
+
193
+ router.delete("/dags/:id", async (req, res) => {
194
+ try {
195
+ const dags = await readDags();
196
+ const idx = dags.findIndex((d) => d.id === req.params.id);
197
+ if (idx === -1) return res.status(404).json({ error: "DAG not found" });
198
+ dags.splice(idx, 1);
199
+ await writeDags(dags);
200
+ res.json({ ok: true });
201
+ } catch (err) {
202
+ res.status(500).json({ error: err.message });
203
+ }
204
+ });
205
+
206
+ // ── Agents CRUD ──
207
+
208
+ router.get("/", async (req, res) => {
209
+ try {
210
+ res.json(await readAgents());
211
+ } catch (err) {
212
+ res.status(500).json({ error: err.message });
213
+ }
214
+ });
215
+
216
+ router.get("/:id", async (req, res) => {
217
+ try {
218
+ const agents = await readAgents();
219
+ const agent = agents.find((a) => a.id === req.params.id);
220
+ if (!agent) return res.status(404).json({ error: "Agent not found" });
221
+ res.json(agent);
222
+ } catch (err) {
223
+ res.status(500).json({ error: err.message });
224
+ }
225
+ });
226
+
227
+ router.post("/", async (req, res) => {
228
+ try {
229
+ const { title, description, goal, icon, constraints } = req.body;
230
+ if (!title || !goal) {
231
+ return res.status(400).json({ error: "title and goal are required" });
232
+ }
233
+ const agents = await readAgents();
234
+ const id = req.body.id || slugify(title);
235
+ if (agents.find((a) => a.id === id)) {
236
+ return res.status(409).json({ error: `Agent with id "${id}" already exists` });
237
+ }
238
+ const agent = {
239
+ id,
240
+ title,
241
+ description: description || "",
242
+ icon: icon || "tool",
243
+ goal,
244
+ custom: true,
245
+ constraints: {
246
+ maxTurns: constraints?.maxTurns || 50,
247
+ timeoutMs: constraints?.timeoutMs || 300000,
248
+ },
249
+ };
250
+ agents.push(agent);
251
+ await writeAgents(agents);
252
+ res.json(agent);
253
+ } catch (err) {
254
+ res.status(500).json({ error: err.message });
255
+ }
256
+ });
257
+
258
+ router.put("/:id", async (req, res) => {
259
+ try {
260
+ const agents = await readAgents();
261
+ const idx = agents.findIndex((a) => a.id === req.params.id);
262
+ if (idx === -1) return res.status(404).json({ error: "Agent not found" });
263
+ const { title, description, goal, icon, constraints } = req.body;
264
+ if (title !== undefined) agents[idx].title = title;
265
+ if (description !== undefined) agents[idx].description = description;
266
+ if (goal !== undefined) agents[idx].goal = goal;
267
+ if (icon !== undefined) agents[idx].icon = icon;
268
+ if (constraints) {
269
+ agents[idx].constraints = {
270
+ ...agents[idx].constraints,
271
+ ...constraints,
272
+ };
273
+ }
274
+ await writeAgents(agents);
275
+ res.json(agents[idx]);
276
+ } catch (err) {
277
+ res.status(500).json({ error: err.message });
278
+ }
279
+ });
280
+
281
+ router.delete("/:id", async (req, res) => {
282
+ try {
283
+ const agents = await readAgents();
284
+ const idx = agents.findIndex((a) => a.id === req.params.id);
285
+ if (idx === -1) return res.status(404).json({ error: "Agent not found" });
286
+ agents.splice(idx, 1);
287
+ await writeAgents(agents);
288
+ res.json({ ok: true });
289
+ } catch (err) {
290
+ res.status(500).json({ error: err.message });
291
+ }
292
+ });
293
+
294
+ export default router;
@@ -0,0 +1,45 @@
1
+ import { Router } from "express";
2
+ import { readFile, writeFile } from "fs/promises";
3
+ import { configPath } from "../paths.js";
4
+
5
+ const dataFile = configPath("bot-prompt.json");
6
+
7
+ const DEFAULT_PROMPT = "You are an expert prompt engineer and AI assistant. Help users craft effective prompts, improve existing ones, and explain prompt engineering techniques. Be concise and actionable.";
8
+
9
+ const router = Router();
10
+
11
+ async function readPromptData() {
12
+ try {
13
+ const raw = await readFile(dataFile, "utf-8");
14
+ return JSON.parse(raw);
15
+ } catch {
16
+ return { systemPrompt: DEFAULT_PROMPT };
17
+ }
18
+ }
19
+
20
+ // GET /prompt — return bot system prompt
21
+ router.get("/prompt", async (req, res) => {
22
+ try {
23
+ const data = await readPromptData();
24
+ res.json(data);
25
+ } catch (err) {
26
+ res.status(500).json({ error: err.message });
27
+ }
28
+ });
29
+
30
+ // PUT /prompt — update bot system prompt
31
+ router.put("/prompt", async (req, res) => {
32
+ try {
33
+ const { systemPrompt } = req.body;
34
+ if (typeof systemPrompt !== "string") {
35
+ return res.status(400).json({ error: "systemPrompt must be a string" });
36
+ }
37
+ const data = { systemPrompt };
38
+ await writeFile(dataFile, JSON.stringify(data, null, 2) + "\n");
39
+ res.json({ ok: true });
40
+ } catch (err) {
41
+ res.status(500).json({ error: err.message });
42
+ }
43
+ });
44
+
45
+ export default router;
@@ -0,0 +1,35 @@
1
+ import { Router } from "express";
2
+ import { exec, execFile } from "child_process";
3
+ import { homedir } from "os";
4
+
5
+ const router = Router();
6
+
7
+ router.post("/", (req, res) => {
8
+ const { command, cwd } = req.body;
9
+ if (!command) return res.status(400).json({ error: "command is required" });
10
+
11
+ const execOpts = {
12
+ cwd: cwd || homedir(),
13
+ timeout: 30000,
14
+ maxBuffer: 512 * 1024,
15
+ };
16
+
17
+ const callback = (err, stdout, stderr) => {
18
+ res.json({
19
+ command,
20
+ stdout: stdout || "",
21
+ stderr: stderr || "",
22
+ exitCode: err ? (err.code ?? 1) : 0,
23
+ });
24
+ };
25
+
26
+ // Use execFile for simple "binary ." commands to avoid shell escaping issues
27
+ const parts = command.split(/\s+/);
28
+ if (parts.length <= 2 && !command.includes("|") && !command.includes(">") && !command.includes("&")) {
29
+ execFile(parts[0], parts.slice(1), execOpts, callback);
30
+ } else {
31
+ exec(command, execOpts, callback);
32
+ }
33
+ });
34
+
35
+ export default router;
@@ -0,0 +1,218 @@
1
+ import { Router } from "express";
2
+ import { readdir, readFile } from "fs/promises";
3
+ import { join, posix, resolve, sep } from "path";
4
+
5
+ const router = Router();
6
+
7
+ // File listing for attachments (recursive, max depth 3)
8
+ router.get("/", async (req, res) => {
9
+ const basePath = req.query.path;
10
+ if (!basePath) return res.status(400).json({ error: "path query param required" });
11
+
12
+ const SKIP = new Set([".git", "node_modules", ".next", "dist", "build", ".cache", ".turbo", "__pycache__", ".venv", "venv", "coverage", ".nyc_output"]);
13
+ const MAX_DEPTH = 3;
14
+ const MAX_FILES = 500;
15
+ const results = [];
16
+
17
+ async function walk(dir, depth) {
18
+ if (depth > MAX_DEPTH || results.length >= MAX_FILES) return;
19
+ try {
20
+ const entries = await readdir(dir, { withFileTypes: true });
21
+ for (const entry of entries) {
22
+ if (results.length >= MAX_FILES) break;
23
+ if (SKIP.has(entry.name)) continue;
24
+ const full = join(dir, entry.name);
25
+ const rel = full.slice(basePath.length + 1);
26
+ if (entry.isDirectory()) {
27
+ await walk(full, depth + 1);
28
+ } else {
29
+ results.push(rel);
30
+ }
31
+ }
32
+ } catch { /* permission errors etc */ }
33
+ }
34
+
35
+ try {
36
+ await walk(basePath, 0);
37
+ res.json(results);
38
+ } catch (err) {
39
+ res.status(500).json({ error: err.message });
40
+ }
41
+ });
42
+
43
+ // Read file content for attachments (50KB limit)
44
+ router.get("/content", async (req, res) => {
45
+ const base = req.query.base;
46
+ const filePath = req.query.path;
47
+ if (!base || !filePath) return res.status(400).json({ error: "base and path required" });
48
+
49
+ const resolved = resolve(base, filePath);
50
+ if (!resolved.startsWith(resolve(base) + sep) && resolved !== resolve(base)) return res.status(403).json({ error: "path traversal detected" });
51
+
52
+ try {
53
+ const { stat } = await import("fs/promises");
54
+ const stats = await stat(resolved);
55
+ if (stats.size > 50 * 1024) {
56
+ return res.status(413).json({ error: "File too large (50KB limit)" });
57
+ }
58
+ const content = await readFile(resolved, "utf-8");
59
+ res.json({ content, path: filePath });
60
+ } catch (err) {
61
+ res.status(404).json({ error: err.message });
62
+ }
63
+ });
64
+
65
+ // File tree (immediate children only, for lazy-loading explorer)
66
+ router.get("/tree", async (req, res) => {
67
+ const base = req.query.base;
68
+ const dir = req.query.dir || "";
69
+ if (!base) return res.status(400).json({ error: "base query param required" });
70
+
71
+ const SKIP = new Set([".git", "node_modules", ".next", "dist", "build", ".cache", ".turbo", "__pycache__", ".venv", "venv", "coverage", ".nyc_output"]);
72
+
73
+ const target = dir ? resolve(base, dir) : resolve(base);
74
+ const resolvedBase = resolve(base);
75
+
76
+ // Path traversal protection
77
+ if (!target.startsWith(resolvedBase + sep) && target !== resolvedBase) {
78
+ return res.status(403).json({ error: "path traversal detected" });
79
+ }
80
+
81
+ try {
82
+ const entries = await readdir(target, { withFileTypes: true });
83
+ const results = [];
84
+
85
+ for (const entry of entries) {
86
+ if (SKIP.has(entry.name)) continue;
87
+ const relPath = dir ? posix.join(dir, entry.name) : entry.name;
88
+ results.push({
89
+ name: entry.name,
90
+ path: relPath,
91
+ type: entry.isDirectory() ? "dir" : "file",
92
+ });
93
+ }
94
+
95
+ // Sort: directories first, then alphabetical
96
+ results.sort((a, b) => {
97
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
98
+ return a.name.localeCompare(b.name);
99
+ });
100
+
101
+ res.json(results);
102
+ } catch (err) {
103
+ if (err.code === "ENOENT") {
104
+ return res.json([]);
105
+ }
106
+ res.status(500).json({ error: err.message });
107
+ }
108
+ });
109
+
110
+ // Serve raw binary files (images) with streaming
111
+ const IMAGE_MIME = {
112
+ ".png": "image/png",
113
+ ".jpg": "image/jpeg",
114
+ ".jpeg": "image/jpeg",
115
+ ".gif": "image/gif",
116
+ ".svg": "image/svg+xml",
117
+ ".webp": "image/webp",
118
+ };
119
+
120
+ router.get("/raw", async (req, res) => {
121
+ const base = req.query.base;
122
+ const filePath = req.query.path;
123
+ if (!base || !filePath) return res.status(400).json({ error: "base and path required" });
124
+
125
+ const resolved = resolve(base, filePath);
126
+ if (!resolved.startsWith(resolve(base) + sep) && resolved !== resolve(base)) return res.status(403).json({ error: "path traversal detected" });
127
+
128
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
129
+ const mime = IMAGE_MIME[ext];
130
+ if (!mime) return res.status(415).json({ error: "unsupported file type" });
131
+
132
+ try {
133
+ const { stat } = await import("fs/promises");
134
+ const stats = await stat(resolved);
135
+ if (stats.size > 5 * 1024 * 1024) {
136
+ return res.status(413).json({ error: "File too large (5MB limit)" });
137
+ }
138
+ res.type(mime).sendFile(resolved);
139
+ } catch (err) {
140
+ res.status(404).json({ error: err.message });
141
+ }
142
+ });
143
+
144
+ // Search files/folders by name (recursive, LIKE %query%)
145
+ router.get("/search", async (req, res) => {
146
+ const base = req.query.base;
147
+ const q = (req.query.q || "").toLowerCase();
148
+ if (!base) return res.status(400).json({ error: "base query param required" });
149
+ if (!q) return res.json([]);
150
+
151
+ const SKIP = new Set([".git", "node_modules", ".next", "dist", "build", ".cache", ".turbo", "__pycache__", ".venv", "venv", "coverage", ".nyc_output"]);
152
+ const MAX_DEPTH = 8;
153
+ const MAX_RESULTS = 50;
154
+ const results = [];
155
+
156
+ async function walk(dir, relDir, depth) {
157
+ if (depth > MAX_DEPTH || results.length >= MAX_RESULTS) return;
158
+ try {
159
+ const entries = await readdir(dir, { withFileTypes: true });
160
+ for (const entry of entries) {
161
+ if (results.length >= MAX_RESULTS) break;
162
+ if (SKIP.has(entry.name)) continue;
163
+
164
+ const relPath = relDir ? posix.join(relDir, entry.name) : entry.name;
165
+ const isDir = entry.isDirectory();
166
+
167
+ // Match name (case-insensitive, like SQL LIKE %q%)
168
+ if (entry.name.toLowerCase().includes(q)) {
169
+ results.push({ name: entry.name, path: relPath, type: isDir ? "dir" : "file" });
170
+ }
171
+
172
+ if (isDir) {
173
+ await walk(join(dir, entry.name), relPath, depth + 1);
174
+ }
175
+ }
176
+ } catch { /* permission errors */ }
177
+ }
178
+
179
+ try {
180
+ await walk(base, "", 0);
181
+ // Sort: directories first, then alphabetical
182
+ results.sort((a, b) => {
183
+ if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
184
+ return a.name.localeCompare(b.name);
185
+ });
186
+ res.json(results);
187
+ } catch (err) {
188
+ res.status(500).json({ error: err.message });
189
+ }
190
+ });
191
+
192
+ // Write file content (for CLAUDE.md editor etc.)
193
+ router.put("/content", async (req, res) => {
194
+ const { base, path: filePath, content } = req.body;
195
+ if (!base || !filePath) return res.status(400).json({ error: "base and path required" });
196
+ if (typeof content !== "string") return res.status(400).json({ error: "content must be a string" });
197
+
198
+ const resolved = resolve(base, filePath);
199
+ if (!resolved.startsWith(resolve(base) + sep) && resolved !== resolve(base)) return res.status(403).json({ error: "path traversal detected" });
200
+
201
+ // Only allow writing specific config files for safety
202
+ const ALLOWED_FILES = new Set(["CLAUDE.md", ".claude/settings.json"]);
203
+ if (!ALLOWED_FILES.has(filePath)) {
204
+ return res.status(403).json({ error: "writing this file is not allowed" });
205
+ }
206
+
207
+ try {
208
+ const { writeFile, mkdir } = await import("fs/promises");
209
+ const { dirname } = await import("path");
210
+ await mkdir(dirname(resolved), { recursive: true });
211
+ await writeFile(resolved, content, "utf-8");
212
+ res.json({ ok: true, path: filePath });
213
+ } catch (err) {
214
+ res.status(500).json({ error: err.message });
215
+ }
216
+ });
217
+
218
+ export default router;
@@ -0,0 +1,82 @@
1
+ import { Router } from "express";
2
+ import { readFile, writeFile, mkdir } from "fs/promises";
3
+ import { join, dirname, isAbsolute } from "path";
4
+ import { homedir } from "os";
5
+
6
+ const router = Router();
7
+
8
+ const GLOBAL_SETTINGS = join(homedir(), ".claude", "settings.json");
9
+
10
+ function getSettingsPath(projectPath) {
11
+ if (projectPath) {
12
+ if (!isAbsolute(projectPath) || projectPath.includes("..")) {
13
+ throw new Error("Invalid project path");
14
+ }
15
+ return join(projectPath, ".claude", "settings.json");
16
+ }
17
+ return GLOBAL_SETTINGS;
18
+ }
19
+
20
+ async function readSettings(settingsPath) {
21
+ try {
22
+ const content = await readFile(settingsPath, "utf-8");
23
+ return JSON.parse(content);
24
+ } catch (err) {
25
+ if (err.code === "ENOENT") return {};
26
+ throw err;
27
+ }
28
+ }
29
+
30
+ async function writeSettings(settingsPath, settings) {
31
+ await mkdir(dirname(settingsPath), { recursive: true });
32
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
33
+ }
34
+
35
+ // List MCP servers (optional ?project=<path> for project-scoped)
36
+ router.get("/servers", async (req, res) => {
37
+ try {
38
+ const settingsPath = getSettingsPath(req.query.project);
39
+ const settings = await readSettings(settingsPath);
40
+ res.json({ servers: settings.mcpServers || {} });
41
+ } catch (err) {
42
+ res.status(500).json({ error: err.message });
43
+ }
44
+ });
45
+
46
+ // Create or update an MCP server
47
+ router.put("/servers/:name", async (req, res) => {
48
+ try {
49
+ const { name } = req.params;
50
+ const config = req.body;
51
+ if (!config || typeof config !== "object") {
52
+ return res.status(400).json({ error: "Invalid config" });
53
+ }
54
+
55
+ const settingsPath = getSettingsPath(req.query.project);
56
+ const settings = await readSettings(settingsPath);
57
+ if (!settings.mcpServers) settings.mcpServers = {};
58
+ settings.mcpServers[name] = config;
59
+ await writeSettings(settingsPath, settings);
60
+ res.json({ success: true });
61
+ } catch (err) {
62
+ res.status(500).json({ error: err.message });
63
+ }
64
+ });
65
+
66
+ // Delete an MCP server
67
+ router.delete("/servers/:name", async (req, res) => {
68
+ try {
69
+ const { name } = req.params;
70
+ const settingsPath = getSettingsPath(req.query.project);
71
+ const settings = await readSettings(settingsPath);
72
+ if (settings.mcpServers) {
73
+ delete settings.mcpServers[name];
74
+ await writeSettings(settingsPath, settings);
75
+ }
76
+ res.json({ success: true });
77
+ } catch (err) {
78
+ res.status(500).json({ error: err.message });
79
+ }
80
+ });
81
+
82
+ export default router;