claudeck 1.4.0 → 1.4.2

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 (61) hide show
  1. package/README.md +6 -8
  2. package/package.json +1 -1
  3. package/plugins/claude-editor/manifest.json +10 -0
  4. package/plugins/repos/manifest.json +10 -0
  5. package/public/css/core/theme.css +6 -21
  6. package/public/css/core/variables.css +2 -0
  7. package/public/css/features/message-queue.css +348 -0
  8. package/public/css/ui/commands.css +4 -4
  9. package/public/css/ui/messages.css +310 -78
  10. package/public/css/ui/right-panel.css +207 -0
  11. package/public/css/ui/sessions.css +173 -0
  12. package/public/css/ui/settings.css +75 -0
  13. package/public/index.html +10 -2
  14. package/public/js/components/add-project-modal.js +14 -0
  15. package/public/js/components/jump-to-latest.js +42 -0
  16. package/public/js/components/queue-stop-modal.js +23 -0
  17. package/public/js/components/settings-modal.js +65 -0
  18. package/public/js/core/api.js +15 -43
  19. package/public/js/core/dom.js +17 -0
  20. package/public/js/core/events.js +11 -0
  21. package/public/js/core/plugin-loader.js +96 -11
  22. package/public/js/core/store.js +11 -0
  23. package/public/js/core/utils.js +38 -2
  24. package/public/js/features/chat.js +49 -1
  25. package/public/js/features/message-queue.js +423 -0
  26. package/public/js/features/projects.js +185 -3
  27. package/public/js/main.js +4 -1
  28. package/public/js/panels/assistant-bot.js +16 -0
  29. package/public/js/panels/dev-docs.js +2 -2
  30. package/public/js/panels/memory.js +1 -0
  31. package/public/js/ui/context-gauge.js +10 -1
  32. package/public/js/ui/formatting.js +65 -11
  33. package/public/js/ui/header-dropdowns.js +30 -0
  34. package/public/js/ui/input-meta.js +13 -6
  35. package/public/js/ui/max-turns.js +6 -3
  36. package/public/js/ui/messages.js +97 -1
  37. package/public/js/ui/model-selector.js +1 -0
  38. package/public/js/ui/parallel.js +32 -2
  39. package/public/js/ui/permissions.js +1 -0
  40. package/public/js/ui/right-panel.js +0 -8
  41. package/public/js/ui/tab-sdk.js +395 -176
  42. package/public/style.css +2 -0
  43. package/server/memory-optimizer.js +17 -13
  44. package/server/routes/marketplace.js +316 -0
  45. package/server/routes/projects.js +0 -0
  46. package/server/ws-handler.js +22 -15
  47. package/server.js +18 -0
  48. package/plugins/event-stream/client.css +0 -207
  49. package/plugins/event-stream/client.js +0 -271
  50. package/plugins/linear/client.css +0 -345
  51. package/plugins/linear/client.js +0 -380
  52. package/plugins/linear/config.json +0 -5
  53. package/plugins/linear/server.js +0 -312
  54. package/plugins/sudoku/client.css +0 -196
  55. package/plugins/sudoku/client.js +0 -329
  56. package/plugins/tasks/client.css +0 -414
  57. package/plugins/tasks/client.js +0 -394
  58. package/plugins/tasks/server.js +0 -116
  59. package/plugins/tic-tac-toe/client.css +0 -167
  60. package/plugins/tic-tac-toe/client.js +0 -241
  61. package/public/js/components/linear-create-modal.js +0 -43
package/public/style.css CHANGED
@@ -24,6 +24,7 @@
24
24
  @import url("css/panels/git-panel.css");
25
25
 
26
26
  @import url("css/panels/mcp-manager.css");
27
+ @import url("css/ui/settings.css");
27
28
  @import url("css/ui/image-attachments.css");
28
29
  @import url("css/panels/tips-feed.css");
29
30
  @import url("css/ui/context-gauge.css");
@@ -41,6 +42,7 @@
41
42
  @import url("css/panels/skills-manager.css");
42
43
  @import url("css/features/telegram.css");
43
44
  @import url("css/features/voice-input.css");
45
+ @import url("css/features/message-queue.css");
44
46
  @import url("css/features/retro-terminal.css");
45
47
  @import url("css/features/welcome.css");
46
48
  @import url("css/features/tour.css");
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { query } from "@anthropic-ai/claude-code";
9
9
  import { execPath } from "process";
10
- import { listMemories, createMemory, deleteMemory, getDb } from "../db.js";
10
+ import { listMemories, getDb } from "../db.js";
11
11
 
12
12
  const VALID_CATEGORIES = new Set(["convention", "decision", "discovery", "warning"]);
13
13
 
@@ -252,28 +252,32 @@ export async function optimizeMemories(projectPath, onProgress = () => {}) {
252
252
  * @returns {{ deleted: number, created: number }}
253
253
  */
254
254
  export async function applyOptimization(projectPath, optimized) {
255
+ const existing = await listMemories(projectPath);
255
256
  const db = getDb();
256
257
 
257
- // Run in a transaction for atomicity
258
- const apply = db.transaction(() => {
259
- // 1. Delete all existing memories for this project
260
- const existing = listMemories(projectPath);
261
- for (const m of existing) {
262
- deleteMemory(m.id);
258
+ // Atomic transaction deletes + inserts succeed or fail together
259
+ const applyTxn = db.transaction((existingRows, newRows) => {
260
+ const del = db.prepare(`DELETE FROM memories WHERE id = ?`);
261
+ const ins = db.prepare(
262
+ `INSERT OR IGNORE INTO memories (project_path, category, content, content_hash, source_session_id, source_agent_id)
263
+ VALUES (?, ?, ?, ?, ?, ?)`
264
+ );
265
+
266
+ for (const m of existingRows) {
267
+ del.run(m.id);
263
268
  }
264
269
 
265
- // 2. Insert optimized memories
266
270
  let created = 0;
267
- for (const { category, content } of optimized) {
271
+ for (const { category, content } of newRows) {
268
272
  if (content && content.trim()) {
269
273
  const cat = VALID_CATEGORIES.has(category) ? category : "discovery";
270
- createMemory(projectPath, cat, content.trim(), null, "optimizer");
274
+ ins.run(projectPath, cat, content.trim(), null, null, "optimizer");
271
275
  created++;
272
276
  }
273
277
  }
274
-
275
- return { deleted: existing.length, created };
278
+ return created;
276
279
  });
277
280
 
278
- return apply();
281
+ const created = applyTxn(existing, optimized);
282
+ return { deleted: existing.length, created };
279
283
  }
@@ -0,0 +1,316 @@
1
+ // Marketplace routes — fetch community plugin registry, install/uninstall plugins
2
+ import { Router } from "express";
3
+ import { join } from "path";
4
+ import { existsSync, mkdirSync, rmSync, readdirSync, readFileSync, writeFileSync, statSync } from "fs";
5
+ import { pathToFileURL } from "url";
6
+ import { userPluginsDir, userConfigDir, packageRoot } from "../paths.js";
7
+
8
+ const router = Router();
9
+
10
+ // Reference to Express app (set during mount for hot-mounting plugin server routes)
11
+ let _app = null;
12
+ export function setApp(app) { _app = app; }
13
+
14
+ // Track hot-mounted plugin routers so we can swap/remove them on uninstall
15
+ const mountedPluginRouters = new Map();
16
+
17
+ // ── Config ──────────────────────────────────────────────────
18
+ const MARKETPLACE_REPO = "hamedafarag/claudeck-marketplace";
19
+ const MARKETPLACE_BRANCH = "main";
20
+ const MARKETPLACE_FILE = "marketplace.json";
21
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
22
+
23
+ let registryCache = null;
24
+ let registryCacheTime = 0;
25
+
26
+ // ── GET /api/marketplace — fetch registry ───────────────────
27
+ router.get("/", async (req, res) => {
28
+ try {
29
+ const now = Date.now();
30
+ const force = req.query.refresh === "true";
31
+
32
+ if (!force && registryCache && now - registryCacheTime < CACHE_TTL) {
33
+ return res.json(enrichRegistry(registryCache));
34
+ }
35
+
36
+ const url = `https://raw.githubusercontent.com/${MARKETPLACE_REPO}/${MARKETPLACE_BRANCH}/${MARKETPLACE_FILE}`;
37
+ const resp = await fetch(url);
38
+ if (!resp.ok) {
39
+ // Return cached data if available, empty otherwise
40
+ if (registryCache) return res.json(enrichRegistry(registryCache));
41
+ return res.json({ name: "claudeck-marketplace", version: "0.0.0", plugins: [] });
42
+ }
43
+
44
+ registryCache = await resp.json();
45
+ registryCacheTime = now;
46
+ res.json(enrichRegistry(registryCache));
47
+ } catch (err) {
48
+ console.error("Marketplace fetch error:", err.message);
49
+ if (registryCache) return res.json(enrichRegistry(registryCache));
50
+ res.json({ name: "claudeck-marketplace", version: "0.0.0", plugins: [] });
51
+ }
52
+ });
53
+
54
+ // ── GET /api/marketplace/installed — list installed community plugins ──
55
+ router.get("/installed", (req, res) => {
56
+ const installed = [];
57
+ if (!existsSync(userPluginsDir)) return res.json(installed);
58
+
59
+ for (const name of readdirSync(userPluginsDir)) {
60
+ const dir = join(userPluginsDir, name);
61
+ if (!statSync(dir).isDirectory()) continue;
62
+ const manifestPath = join(dir, "manifest.json");
63
+ if (!existsSync(manifestPath)) continue;
64
+ try {
65
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
66
+ // Only list community plugins (those with _marketplace marker)
67
+ const metaPath = join(dir, ".marketplace");
68
+ if (existsSync(metaPath)) {
69
+ const meta = JSON.parse(readFileSync(metaPath, "utf8"));
70
+ installed.push({ ...manifest, installedFrom: meta.source, installedAt: meta.installedAt });
71
+ }
72
+ } catch {}
73
+ }
74
+ res.json(installed);
75
+ });
76
+
77
+ // Validate plugin ID to prevent path traversal
78
+ const SAFE_ID = /^[a-z0-9][a-z0-9-]*$/;
79
+
80
+ // ── POST /api/marketplace/install — install a community plugin ──
81
+ router.post("/install", async (req, res) => {
82
+ const { id, repo, source } = req.body;
83
+ if (!id) return res.status(400).json({ error: "Plugin id required" });
84
+ if (!SAFE_ID.test(id)) return res.status(400).json({ error: "Invalid plugin id" });
85
+
86
+ const pluginDir = join(userPluginsDir, id);
87
+
88
+ try {
89
+ // Determine download source
90
+ let downloadUrl;
91
+ let isMonorepo = false;
92
+
93
+ if (source && source.startsWith("./")) {
94
+ // Monorepo plugin — download from marketplace repo subdirectory
95
+ isMonorepo = true;
96
+ downloadUrl = `https://api.github.com/repos/${MARKETPLACE_REPO}/tarball/${MARKETPLACE_BRANCH}`;
97
+ } else if (repo) {
98
+ // External repo — download tarball
99
+ downloadUrl = `https://api.github.com/repos/${repo}/tarball`;
100
+ } else {
101
+ return res.status(400).json({ error: "Plugin must have 'repo' or 'source'" });
102
+ }
103
+
104
+ // Download tarball
105
+ const resp = await fetch(downloadUrl, {
106
+ headers: { "Accept": "application/vnd.github+json", "User-Agent": "claudeck" },
107
+ redirect: "follow",
108
+ });
109
+ if (!resp.ok) {
110
+ return res.status(502).json({ error: `GitHub download failed: ${resp.status} ${resp.statusText}` });
111
+ }
112
+
113
+ // Write tarball to temp file
114
+ const tmpTar = join(userPluginsDir, `_tmp_${id}.tar.gz`);
115
+ const tmpDir = join(userPluginsDir, `_tmp_${id}`);
116
+ const buffer = Buffer.from(await resp.arrayBuffer());
117
+ writeFileSync(tmpTar, buffer);
118
+
119
+ // Extract tarball
120
+ mkdirSync(tmpDir, { recursive: true });
121
+ const { execSync } = await import("child_process");
122
+ execSync(`tar -xzf "${tmpTar}" -C "${tmpDir}" --no-same-owner --no-same-permissions`, { stdio: "pipe" });
123
+
124
+ // Find extracted content (tarball has a root dir like owner-repo-sha/)
125
+ const extracted = readdirSync(tmpDir);
126
+ const rootDir = extracted.length === 1 ? join(tmpDir, extracted[0]) : tmpDir;
127
+
128
+ // Determine source directory within extracted content
129
+ let sourceDir;
130
+ if (isMonorepo) {
131
+ // For monorepo plugins, navigate to the plugin subdirectory
132
+ const subPath = source.replace("./", "");
133
+ sourceDir = join(rootDir, subPath);
134
+ if (!existsSync(sourceDir) || !existsSync(join(sourceDir, "client.js"))) {
135
+ // Cleanup
136
+ rmSync(tmpTar, { force: true });
137
+ rmSync(tmpDir, { recursive: true, force: true });
138
+ return res.status(404).json({ error: `Plugin directory not found in marketplace repo: ${subPath}` });
139
+ }
140
+ } else {
141
+ // External repo — root is the plugin
142
+ sourceDir = rootDir;
143
+ if (!existsSync(join(sourceDir, "client.js"))) {
144
+ rmSync(tmpTar, { force: true });
145
+ rmSync(tmpDir, { recursive: true, force: true });
146
+ return res.status(400).json({ error: "Plugin repo must contain client.js at root" });
147
+ }
148
+ }
149
+
150
+ // Remove old installation if exists
151
+ if (existsSync(pluginDir)) {
152
+ rmSync(pluginDir, { recursive: true, force: true });
153
+ }
154
+
155
+ // Move plugin to final location
156
+ const { cpSync } = await import("fs");
157
+ cpSync(sourceDir, pluginDir, { recursive: true });
158
+
159
+ // Patch server.js imports: rewrite relative db.js imports to absolute path
160
+ const serverFilePath = join(pluginDir, "server.js");
161
+ if (existsSync(serverFilePath)) {
162
+ try {
163
+ let code = readFileSync(serverFilePath, "utf8");
164
+ const dbAbsolute = pathToFileURL(join(packageRoot, "db.js")).href;
165
+ // Replace relative imports like ../../db.js, ../db.js, etc.
166
+ code = code.replace(
167
+ /from\s+["'](?:\.\.\/)+db\.js["']/g,
168
+ `from "${dbAbsolute}"`
169
+ );
170
+ writeFileSync(serverFilePath, code);
171
+ } catch (err) {
172
+ console.warn(`Could not patch server.js imports for ${id}:`, err.message);
173
+ }
174
+ }
175
+
176
+ // Write marketplace metadata
177
+ writeFileSync(join(pluginDir, ".marketplace"), JSON.stringify({
178
+ source: repo || source,
179
+ installedAt: new Date().toISOString(),
180
+ marketplace: MARKETPLACE_REPO,
181
+ }));
182
+
183
+ // Copy default config if plugin ships one
184
+ const configSrc = join(pluginDir, "config.json");
185
+ const configDst = join(userConfigDir, `${id}-config.json`);
186
+ if (existsSync(configSrc) && !existsSync(configDst)) {
187
+ const { copyFileSync } = await import("fs");
188
+ copyFileSync(configSrc, configDst);
189
+ }
190
+
191
+ // Cleanup temp files
192
+ rmSync(tmpTar, { force: true });
193
+ rmSync(tmpDir, { recursive: true, force: true });
194
+
195
+ // Read installed manifest
196
+ const manifestPath = join(pluginDir, "manifest.json");
197
+ let manifest = null;
198
+ if (existsSync(manifestPath)) {
199
+ try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")); } catch {}
200
+ }
201
+
202
+ // Hot-mount server routes if plugin has server.js and app is available
203
+ let serverMounted = false;
204
+ const serverFile = join(pluginDir, "server.js");
205
+ if (_app && existsSync(serverFile)) {
206
+ const allowUserServer = process.env.CLAUDECK_USER_SERVER_PLUGINS === "true";
207
+ if (allowUserServer) {
208
+ try {
209
+ // Use cache-busting query to force re-import on reinstall
210
+ const mod = await import(pathToFileURL(serverFile).href + `?t=${Date.now()}`);
211
+
212
+ // If already mounted, swap the inner handler; otherwise mount a wrapper
213
+ if (mountedPluginRouters.has(id)) {
214
+ mountedPluginRouters.get(id).inner = mod.default;
215
+ } else {
216
+ const wrapper = { inner: mod.default };
217
+ const wrapperRouter = Router();
218
+ wrapperRouter.use((req, res, next) => {
219
+ if (wrapper.inner) wrapper.inner(req, res, next);
220
+ else next();
221
+ });
222
+ _app.use(`/api/plugins/${id}`, wrapperRouter);
223
+ mountedPluginRouters.set(id, wrapper);
224
+ }
225
+ serverMounted = true;
226
+ console.log(`Hot-mounted plugin routes: /api/plugins/${id}`);
227
+ } catch (err) {
228
+ console.error(`Failed to hot-mount plugin server: ${id}`, err.message);
229
+ }
230
+ }
231
+ }
232
+
233
+ res.json({ ok: true, id, manifest, serverMounted });
234
+ } catch (err) {
235
+ // Cleanup on error
236
+ const tmpTar = join(userPluginsDir, `_tmp_${id}.tar.gz`);
237
+ const tmpDir = join(userPluginsDir, `_tmp_${id}`);
238
+ rmSync(tmpTar, { force: true });
239
+ rmSync(tmpDir, { recursive: true, force: true });
240
+ console.error(`Marketplace install error (${id}):`, err.message);
241
+ res.status(500).json({ error: err.message });
242
+ }
243
+ });
244
+
245
+ // ── POST /api/marketplace/uninstall — remove a community plugin ──
246
+ router.post("/uninstall", (req, res) => {
247
+ const { id } = req.body;
248
+ if (!id) return res.status(400).json({ error: "Plugin id required" });
249
+ if (!SAFE_ID.test(id)) return res.status(400).json({ error: "Invalid plugin id" });
250
+
251
+ const pluginDir = join(userPluginsDir, id);
252
+ const marketplaceMeta = join(pluginDir, ".marketplace");
253
+
254
+ // Only allow uninstalling community plugins (has .marketplace marker)
255
+ if (!existsSync(marketplaceMeta)) {
256
+ return res.status(400).json({ error: "Plugin is not a community plugin or not installed" });
257
+ }
258
+
259
+ try {
260
+ rmSync(pluginDir, { recursive: true, force: true });
261
+
262
+ // Disable hot-mounted server routes (wrapper stays but inner is nulled)
263
+ if (mountedPluginRouters.has(id)) {
264
+ mountedPluginRouters.get(id).inner = null;
265
+ console.log(`Disabled plugin routes: /api/plugins/${id}`);
266
+ }
267
+
268
+ res.json({ ok: true, id });
269
+ } catch (err) {
270
+ console.error(`Marketplace uninstall error (${id}):`, err.message);
271
+ res.status(500).json({ error: err.message });
272
+ }
273
+ });
274
+
275
+ // ── Helpers ─────────────────────────────────────────────────
276
+
277
+ /** Enrich registry with installation status and built-in detection */
278
+ function enrichRegistry(registry) {
279
+ const builtinDir = join(packageRoot, "plugins");
280
+ const enriched = { ...registry, plugins: [] };
281
+ for (const plugin of registry.plugins || []) {
282
+ // Check if this plugin ships as a built-in
283
+ const isBuiltin = existsSync(join(builtinDir, plugin.id, "client.js"));
284
+
285
+ const dir = join(userPluginsDir, plugin.id);
286
+ const installed = existsSync(join(dir, ".marketplace"));
287
+ let installedVersion = null;
288
+ if (installed) {
289
+ const mPath = join(dir, "manifest.json");
290
+ if (existsSync(mPath)) {
291
+ try { installedVersion = JSON.parse(readFileSync(mPath, "utf8")).version; } catch {}
292
+ }
293
+ }
294
+ enriched.plugins.push({
295
+ ...plugin,
296
+ isBuiltin,
297
+ installed,
298
+ installedVersion,
299
+ updateAvailable: installed && installedVersion && semverNewer(plugin.version, installedVersion),
300
+ });
301
+ }
302
+ return enriched;
303
+ }
304
+
305
+ /** Return true if `a` is strictly newer than `b` (simple semver comparison) */
306
+ function semverNewer(a, b) {
307
+ const pa = (a || "0.0.0").split(".").map(Number);
308
+ const pb = (b || "0.0.0").split(".").map(Number);
309
+ for (let i = 0; i < 3; i++) {
310
+ if ((pa[i] || 0) > (pb[i] || 0)) return true;
311
+ if ((pa[i] || 0) < (pb[i] || 0)) return false;
312
+ }
313
+ return false;
314
+ }
315
+
316
+ export default router;
Binary file
@@ -895,28 +895,35 @@ export async function handleChat(msg, { ws, sessionIds, activeQueries, pendingAp
895
895
  const sKey = chatId ? `${ourSid}::${chatId}` : ourSid;
896
896
  sessionIds.set(sKey, claudeSessionId);
897
897
 
898
- if (!await getSession(ourSid)) {
899
- await createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
900
- } else {
901
- await updateClaudeSessionId(ourSid, claudeSessionId);
898
+ const isBotChat = chatId === 'assistant-bot';
899
+
900
+ if (!isBotChat) {
901
+ if (!await getSession(ourSid)) {
902
+ await createSession(ourSid, claudeSessionId, projectName || "Session", cwd || "");
903
+ } else {
904
+ await updateClaudeSessionId(ourSid, claudeSessionId);
905
+ }
902
906
  }
903
907
 
904
- if (chatId) {
908
+ if (chatId && !isBotChat) {
905
909
  await setClaudeSession(ourSid, chatId, claudeSessionId);
906
910
  }
907
911
 
908
912
  wsSend({ type: "session", sessionId: ourSid });
909
- const userMsgData = { text: message };
910
- if (images?.length) {
911
- userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
912
- }
913
- await addMessage(state.resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
914
913
 
915
- // Broadcast user message to observers (sender already rendered it locally)
916
- const userBroadcast = { type: "user_message", text: message, sessionId: state.resolvedSid };
917
- if (chatId) userBroadcast.chatId = chatId;
918
- if (images?.length) userBroadcast.images = images.map(i => ({ name: i.name, mimeType: i.mimeType }));
919
- broadcastToSession(state.resolvedSid, userBroadcast, ws);
914
+ if (!isBotChat) {
915
+ const userMsgData = { text: message };
916
+ if (images?.length) {
917
+ userMsgData.images = images.map(i => ({ name: i.name, data: i.data, mimeType: i.mimeType }));
918
+ }
919
+ await addMessage(state.resolvedSid, "user", JSON.stringify(userMsgData), chatId || null);
920
+
921
+ // Broadcast user message to observers (sender already rendered it locally)
922
+ const userBroadcast = { type: "user_message", text: message, sessionId: state.resolvedSid };
923
+ if (chatId) userBroadcast.chatId = chatId;
924
+ if (images?.length) userBroadcast.images = images.map(i => ({ name: i.name, mimeType: i.mimeType }));
925
+ broadcastToSession(state.resolvedSid, userBroadcast, ws);
926
+ }
920
927
 
921
928
  // Register global query tracking now that we know the session
922
929
  if (!clientSid) registerGlobalQuery(state.resolvedSid, queryKey);
package/server.js CHANGED
@@ -32,6 +32,7 @@ import notificationsRouter, { setVapidPublicKey } from "./server/routes/notifica
32
32
  import memoryRouter from "./server/routes/memory.js";
33
33
  import worktreesRouter from "./server/routes/worktrees.js";
34
34
  import skillsRouter from "./server/routes/skills.js";
35
+ import marketplaceRouter, { setApp as setMarketplaceApp } from "./server/routes/marketplace.js";
35
36
  import { setupWebSocket } from "./server/ws-handler.js";
36
37
  import { setWss } from "./server/notification-logger.js";
37
38
  import { authMiddleware, verifyWsClient, isAuthEnabled, getToken, loginHandler, statusHandler } from "./server/auth.js";
@@ -129,6 +130,8 @@ app.use("/api/telegram", telegramRouter);
129
130
  app.use("/api/memory", memoryRouter);
130
131
  app.use("/api/worktrees", worktreesRouter);
131
132
  app.use("/api/skills", skillsRouter);
133
+ app.use("/api/marketplace", marketplaceRouter);
134
+ setMarketplaceApp(app);
132
135
 
133
136
  // Version endpoint
134
137
  import { readFileSync } from "fs";
@@ -154,12 +157,19 @@ app.get("/api/plugins", (req, res) => {
154
157
  if (!existsSync(join(dir, "client.js"))) continue;
155
158
  const hasCss = existsSync(join(dir, "client.css"));
156
159
  const hasServer = existsSync(join(dir, "server.js"));
160
+ // Read manifest.json if it exists
161
+ let manifest = null;
162
+ const manifestPath = join(dir, "manifest.json");
163
+ if (existsSync(manifestPath)) {
164
+ try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")); } catch {}
165
+ }
157
166
  plugins.push({
158
167
  name,
159
168
  js: `plugins/${name}/client.js`,
160
169
  css: hasCss ? `plugins/${name}/client.css` : null,
161
170
  source: "builtin",
162
171
  apiBase: hasServer ? `/api/plugins/${name}` : null,
172
+ manifest,
163
173
  });
164
174
  }
165
175
  }
@@ -173,12 +183,20 @@ app.get("/api/plugins", (req, res) => {
173
183
  const hasCss = existsSync(join(dir, "client.css"));
174
184
  const allowUserServer = process.env.CLAUDECK_USER_SERVER_PLUGINS === "true";
175
185
  const hasServer = allowUserServer && existsSync(join(dir, "server.js"));
186
+ const fromMarketplace = existsSync(join(dir, ".marketplace"));
187
+ let manifest = null;
188
+ const manifestPath = join(dir, "manifest.json");
189
+ if (existsSync(manifestPath)) {
190
+ try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")); } catch {}
191
+ }
176
192
  plugins.push({
177
193
  name: entry,
178
194
  js: `user-plugins/${entry}/client.js`,
179
195
  css: hasCss ? `user-plugins/${entry}/client.css` : null,
180
196
  source: "user",
197
+ fromMarketplace,
181
198
  apiBase: hasServer ? `/api/plugins/${entry}` : null,
199
+ manifest,
182
200
  });
183
201
  }
184
202
  }