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.
- package/README.md +6 -8
- package/package.json +1 -1
- package/plugins/claude-editor/manifest.json +10 -0
- package/plugins/repos/manifest.json +10 -0
- package/public/css/core/theme.css +6 -21
- package/public/css/core/variables.css +2 -0
- package/public/css/features/message-queue.css +348 -0
- package/public/css/ui/commands.css +4 -4
- package/public/css/ui/messages.css +310 -78
- package/public/css/ui/right-panel.css +207 -0
- package/public/css/ui/sessions.css +173 -0
- package/public/css/ui/settings.css +75 -0
- package/public/index.html +10 -2
- package/public/js/components/add-project-modal.js +14 -0
- package/public/js/components/jump-to-latest.js +42 -0
- package/public/js/components/queue-stop-modal.js +23 -0
- package/public/js/components/settings-modal.js +65 -0
- package/public/js/core/api.js +15 -43
- package/public/js/core/dom.js +17 -0
- package/public/js/core/events.js +11 -0
- package/public/js/core/plugin-loader.js +96 -11
- package/public/js/core/store.js +11 -0
- package/public/js/core/utils.js +38 -2
- package/public/js/features/chat.js +49 -1
- package/public/js/features/message-queue.js +423 -0
- package/public/js/features/projects.js +185 -3
- package/public/js/main.js +4 -1
- package/public/js/panels/assistant-bot.js +16 -0
- package/public/js/panels/dev-docs.js +2 -2
- package/public/js/panels/memory.js +1 -0
- package/public/js/ui/context-gauge.js +10 -1
- package/public/js/ui/formatting.js +65 -11
- package/public/js/ui/header-dropdowns.js +30 -0
- package/public/js/ui/input-meta.js +13 -6
- package/public/js/ui/max-turns.js +6 -3
- package/public/js/ui/messages.js +97 -1
- package/public/js/ui/model-selector.js +1 -0
- package/public/js/ui/parallel.js +32 -2
- package/public/js/ui/permissions.js +1 -0
- package/public/js/ui/right-panel.js +0 -8
- package/public/js/ui/tab-sdk.js +395 -176
- package/public/style.css +2 -0
- package/server/memory-optimizer.js +17 -13
- package/server/routes/marketplace.js +316 -0
- package/server/routes/projects.js +0 -0
- package/server/ws-handler.js +22 -15
- package/server.js +18 -0
- package/plugins/event-stream/client.css +0 -207
- package/plugins/event-stream/client.js +0 -271
- package/plugins/linear/client.css +0 -345
- package/plugins/linear/client.js +0 -380
- package/plugins/linear/config.json +0 -5
- package/plugins/linear/server.js +0 -312
- package/plugins/sudoku/client.css +0 -196
- package/plugins/sudoku/client.js +0 -329
- package/plugins/tasks/client.css +0 -414
- package/plugins/tasks/client.js +0 -394
- package/plugins/tasks/server.js +0 -116
- package/plugins/tic-tac-toe/client.css +0 -167
- package/plugins/tic-tac-toe/client.js +0 -241
- 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,
|
|
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
|
-
//
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
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
|
|
271
|
+
for (const { category, content } of newRows) {
|
|
268
272
|
if (content && content.trim()) {
|
|
269
273
|
const cat = VALID_CATEGORIES.has(category) ? category : "discovery";
|
|
270
|
-
|
|
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
|
-
|
|
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
|
package/server/ws-handler.js
CHANGED
|
@@ -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
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
await
|
|
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
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
}
|