@cryptiklemur/lattice 1.25.1 → 1.26.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 +94 -84
- package/client/src/components/project-settings/ProjectPlugins.tsx +117 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/settings/GlobalPlugins.tsx +801 -0
- package/client/src/components/settings/SettingsView.tsx +3 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +3 -1
- package/client/src/stores/sidebar.ts +2 -2
- package/package.json +1 -1
- package/server/src/daemon.ts +1 -0
- package/server/src/handlers/plugins.ts +658 -0
- package/server/src/handlers/project-settings.ts +6 -0
- package/server/src/project/context-breakdown.ts +12 -0
- package/server/src/project/sdk-bridge.ts +6 -0
- package/shared/src/messages.ts +123 -2
- package/shared/src/models.ts +59 -0
- package/shared/src/project-settings.ts +2 -1
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import type {
|
|
6
|
+
ClientMessage,
|
|
7
|
+
PluginInfo,
|
|
8
|
+
PluginMarketplaceInfo,
|
|
9
|
+
PluginDetails,
|
|
10
|
+
PluginError,
|
|
11
|
+
MarketplacePluginEntry,
|
|
12
|
+
} from "@lattice/shared";
|
|
13
|
+
import { registerHandler } from "../ws/router";
|
|
14
|
+
import { sendTo } from "../ws/broadcast";
|
|
15
|
+
|
|
16
|
+
var PLUGINS_DIR = join(homedir(), ".claude", "plugins");
|
|
17
|
+
var INSTALLED_FILE = join(PLUGINS_DIR, "installed_plugins.json");
|
|
18
|
+
var MARKETPLACES_FILE = join(PLUGINS_DIR, "known_marketplaces.json");
|
|
19
|
+
var INSTALL_COUNTS_FILE = join(PLUGINS_DIR, "install-counts-cache.json");
|
|
20
|
+
|
|
21
|
+
interface InstalledPluginsFile {
|
|
22
|
+
version: number;
|
|
23
|
+
plugins: Record<string, Array<{
|
|
24
|
+
scope: string;
|
|
25
|
+
installPath: string;
|
|
26
|
+
version: string;
|
|
27
|
+
installedAt: string;
|
|
28
|
+
lastUpdated: string;
|
|
29
|
+
gitCommitSha: string;
|
|
30
|
+
}>>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PluginJson {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
version?: string;
|
|
37
|
+
author?: { name: string; email?: string };
|
|
38
|
+
homepage?: string;
|
|
39
|
+
license?: string;
|
|
40
|
+
keywords?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface InstallCountsFile {
|
|
44
|
+
version: number;
|
|
45
|
+
fetchedAt: string;
|
|
46
|
+
counts: Array<{ plugin: string; unique_installs: number }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface MarketplacesFile {
|
|
50
|
+
[name: string]: {
|
|
51
|
+
source: { source: string; repo: string };
|
|
52
|
+
installLocation: string;
|
|
53
|
+
lastUpdated: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readJsonFile<T>(path: string): T | null {
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(path)) return null;
|
|
60
|
+
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getInstallCounts(): Map<string, number> {
|
|
67
|
+
var data = readJsonFile<InstallCountsFile>(INSTALL_COUNTS_FILE);
|
|
68
|
+
var map = new Map<string, number>();
|
|
69
|
+
if (!data || !data.counts) return map;
|
|
70
|
+
for (var i = 0; i < data.counts.length; i++) {
|
|
71
|
+
map.set(data.counts[i].plugin, data.counts[i].unique_installs);
|
|
72
|
+
}
|
|
73
|
+
return map;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readPluginJson(installPath: string): PluginJson | null {
|
|
77
|
+
var pluginJsonPath = join(installPath, ".claude-plugin", "plugin.json");
|
|
78
|
+
return readJsonFile<PluginJson>(pluginJsonPath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function countSkills(installPath: string): number {
|
|
82
|
+
var skillsDir = join(installPath, "skills");
|
|
83
|
+
if (!existsSync(skillsDir)) return 0;
|
|
84
|
+
try {
|
|
85
|
+
var entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
86
|
+
var count = 0;
|
|
87
|
+
for (var i = 0; i < entries.length; i++) {
|
|
88
|
+
if (entries[i].isDirectory()) count++;
|
|
89
|
+
}
|
|
90
|
+
return count;
|
|
91
|
+
} catch {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function countHooks(installPath: string): number {
|
|
97
|
+
var hooksPath = join(installPath, "hooks", "hooks.json");
|
|
98
|
+
var data = readJsonFile<{ hooks: Record<string, unknown> }>(hooksPath);
|
|
99
|
+
if (!data || !data.hooks) return 0;
|
|
100
|
+
return Object.keys(data.hooks).length;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function countRules(installPath: string): number {
|
|
104
|
+
var rulesDir = join(installPath, "rules");
|
|
105
|
+
if (!existsSync(rulesDir)) return 0;
|
|
106
|
+
try {
|
|
107
|
+
var entries = readdirSync(rulesDir);
|
|
108
|
+
var count = 0;
|
|
109
|
+
for (var i = 0; i < entries.length; i++) {
|
|
110
|
+
if (entries[i].endsWith(".md")) count++;
|
|
111
|
+
}
|
|
112
|
+
return count;
|
|
113
|
+
} catch {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getInstalledPlugins(): PluginInfo[] {
|
|
119
|
+
var data = readJsonFile<InstalledPluginsFile>(INSTALLED_FILE);
|
|
120
|
+
if (!data || !data.plugins) return [];
|
|
121
|
+
var installCounts = getInstallCounts();
|
|
122
|
+
var plugins: PluginInfo[] = [];
|
|
123
|
+
|
|
124
|
+
var keys = Object.keys(data.plugins);
|
|
125
|
+
for (var k = 0; k < keys.length; k++) {
|
|
126
|
+
var key = keys[k];
|
|
127
|
+
var entries = data.plugins[key];
|
|
128
|
+
var atIdx = key.lastIndexOf("@");
|
|
129
|
+
var pluginName = atIdx > 0 ? key.slice(0, atIdx) : key;
|
|
130
|
+
var marketplace = atIdx > 0 ? key.slice(atIdx + 1) : "";
|
|
131
|
+
|
|
132
|
+
for (var e = 0; e < entries.length; e++) {
|
|
133
|
+
var entry = entries[e];
|
|
134
|
+
var meta = readPluginJson(entry.installPath);
|
|
135
|
+
plugins.push({
|
|
136
|
+
name: pluginName,
|
|
137
|
+
marketplace: marketplace,
|
|
138
|
+
key: key,
|
|
139
|
+
version: entry.version,
|
|
140
|
+
scope: entry.scope,
|
|
141
|
+
installPath: entry.installPath,
|
|
142
|
+
installedAt: entry.installedAt,
|
|
143
|
+
lastUpdated: entry.lastUpdated,
|
|
144
|
+
gitCommitSha: entry.gitCommitSha,
|
|
145
|
+
description: meta?.description ?? "",
|
|
146
|
+
skillCount: countSkills(entry.installPath),
|
|
147
|
+
hookCount: countHooks(entry.installPath),
|
|
148
|
+
ruleCount: countRules(entry.installPath),
|
|
149
|
+
installs: installCounts.get(key),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
plugins.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
155
|
+
return plugins;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getMarketplaces(): PluginMarketplaceInfo[] {
|
|
159
|
+
var data = readJsonFile<MarketplacesFile>(MARKETPLACES_FILE);
|
|
160
|
+
if (!data) return [];
|
|
161
|
+
var result: PluginMarketplaceInfo[] = [];
|
|
162
|
+
var keys = Object.keys(data);
|
|
163
|
+
for (var i = 0; i < keys.length; i++) {
|
|
164
|
+
var entry = data[keys[i]];
|
|
165
|
+
result.push({
|
|
166
|
+
name: keys[i],
|
|
167
|
+
source: entry.source,
|
|
168
|
+
installLocation: entry.installLocation,
|
|
169
|
+
lastUpdated: entry.lastUpdated,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function searchMarketplacePlugins(query: string, marketplaceFilter?: string): MarketplacePluginEntry[] {
|
|
176
|
+
var marketplaces = readJsonFile<MarketplacesFile>(MARKETPLACES_FILE);
|
|
177
|
+
if (!marketplaces) return [];
|
|
178
|
+
var installCounts = getInstallCounts();
|
|
179
|
+
var installedData = readJsonFile<InstalledPluginsFile>(INSTALLED_FILE);
|
|
180
|
+
var installedKeys = new Set<string>();
|
|
181
|
+
var installedVersions = new Map<string, string>();
|
|
182
|
+
if (installedData && installedData.plugins) {
|
|
183
|
+
var iKeys = Object.keys(installedData.plugins);
|
|
184
|
+
for (var ik = 0; ik < iKeys.length; ik++) {
|
|
185
|
+
installedKeys.add(iKeys[ik]);
|
|
186
|
+
var versions = installedData.plugins[iKeys[ik]];
|
|
187
|
+
if (versions.length > 0) {
|
|
188
|
+
installedVersions.set(iKeys[ik], versions[0].version);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var results: MarketplacePluginEntry[] = [];
|
|
194
|
+
var lowerQuery = query.toLowerCase();
|
|
195
|
+
var mKeys = Object.keys(marketplaces);
|
|
196
|
+
|
|
197
|
+
for (var m = 0; m < mKeys.length; m++) {
|
|
198
|
+
var mName = mKeys[m];
|
|
199
|
+
if (marketplaceFilter && mName !== marketplaceFilter) continue;
|
|
200
|
+
|
|
201
|
+
var mkt = marketplaces[mName];
|
|
202
|
+
var pluginsDir = join(mkt.installLocation, "plugins");
|
|
203
|
+
if (!existsSync(pluginsDir)) continue;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
var pluginDirs = readdirSync(pluginsDir, { withFileTypes: true });
|
|
207
|
+
for (var p = 0; p < pluginDirs.length; p++) {
|
|
208
|
+
if (!pluginDirs[p].isDirectory()) continue;
|
|
209
|
+
var dirName = pluginDirs[p].name;
|
|
210
|
+
if (dirName.toLowerCase().indexOf(lowerQuery) === -1) {
|
|
211
|
+
var meta = readPluginJson(join(pluginsDir, dirName));
|
|
212
|
+
if (!meta || meta.description.toLowerCase().indexOf(lowerQuery) === -1) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
var pluginMeta = readPluginJson(join(pluginsDir, dirName));
|
|
218
|
+
var key = dirName + "@" + mName;
|
|
219
|
+
results.push({
|
|
220
|
+
name: dirName,
|
|
221
|
+
marketplace: mName,
|
|
222
|
+
description: pluginMeta?.description ?? "",
|
|
223
|
+
author: pluginMeta?.author,
|
|
224
|
+
installed: installedKeys.has(key),
|
|
225
|
+
installedVersion: installedVersions.get(key),
|
|
226
|
+
installs: installCounts.get(key),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
results.sort(function (a, b) {
|
|
233
|
+
var ai = a.installs ?? 0;
|
|
234
|
+
var bi = b.installs ?? 0;
|
|
235
|
+
return bi - ai;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return results;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseFrontmatter(content: string): { name: string; description: string } {
|
|
242
|
+
var match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
243
|
+
if (!match) return { name: "", description: "" };
|
|
244
|
+
var yaml = match[1];
|
|
245
|
+
var name = "";
|
|
246
|
+
var desc = "";
|
|
247
|
+
var lines = yaml.split(/\r?\n/);
|
|
248
|
+
for (var i = 0; i < lines.length; i++) {
|
|
249
|
+
var line = lines[i];
|
|
250
|
+
var nameMatch = line.match(/^name:\s*(.+)/);
|
|
251
|
+
if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
252
|
+
var descMatch = line.match(/^description:\s*(.+)/);
|
|
253
|
+
if (descMatch) desc = descMatch[1].trim().replace(/^["']|["']$/g, "");
|
|
254
|
+
}
|
|
255
|
+
return { name, description: desc };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getPluginDetails(pluginName: string, marketplace: string): PluginDetails | null {
|
|
259
|
+
var data = readJsonFile<InstalledPluginsFile>(INSTALLED_FILE);
|
|
260
|
+
if (!data || !data.plugins) return null;
|
|
261
|
+
|
|
262
|
+
var key = pluginName + "@" + marketplace;
|
|
263
|
+
var entries = data.plugins[key];
|
|
264
|
+
if (!entries || entries.length === 0) return null;
|
|
265
|
+
|
|
266
|
+
var entry = entries[0];
|
|
267
|
+
var meta = readPluginJson(entry.installPath);
|
|
268
|
+
|
|
269
|
+
var skills: Array<{ name: string; description: string }> = [];
|
|
270
|
+
var skillsDir = join(entry.installPath, "skills");
|
|
271
|
+
if (existsSync(skillsDir)) {
|
|
272
|
+
try {
|
|
273
|
+
var skillDirs = readdirSync(skillsDir, { withFileTypes: true });
|
|
274
|
+
for (var s = 0; s < skillDirs.length; s++) {
|
|
275
|
+
if (!skillDirs[s].isDirectory()) continue;
|
|
276
|
+
var skillFile = join(skillsDir, skillDirs[s].name, "SKILL.md");
|
|
277
|
+
if (existsSync(skillFile)) {
|
|
278
|
+
var content = readFileSync(skillFile, "utf-8");
|
|
279
|
+
var fm = parseFrontmatter(content);
|
|
280
|
+
skills.push({ name: fm.name || skillDirs[s].name, description: fm.description });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} catch {}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
var hooks: Record<string, unknown> = {};
|
|
287
|
+
var hooksPath = join(entry.installPath, "hooks", "hooks.json");
|
|
288
|
+
var hooksData = readJsonFile<{ hooks: Record<string, unknown> }>(hooksPath);
|
|
289
|
+
if (hooksData && hooksData.hooks) {
|
|
290
|
+
hooks = hooksData.hooks;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
var rules: string[] = [];
|
|
294
|
+
var rulesDir = join(entry.installPath, "rules");
|
|
295
|
+
if (existsSync(rulesDir)) {
|
|
296
|
+
try {
|
|
297
|
+
var ruleFiles = readdirSync(rulesDir);
|
|
298
|
+
for (var r = 0; r < ruleFiles.length; r++) {
|
|
299
|
+
if (ruleFiles[r].endsWith(".md")) {
|
|
300
|
+
rules.push(ruleFiles[r]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
name: pluginName,
|
|
308
|
+
marketplace: marketplace,
|
|
309
|
+
version: entry.version,
|
|
310
|
+
description: meta?.description ?? "",
|
|
311
|
+
author: meta?.author,
|
|
312
|
+
homepage: meta?.homepage,
|
|
313
|
+
license: meta?.license,
|
|
314
|
+
keywords: meta?.keywords,
|
|
315
|
+
skills: skills,
|
|
316
|
+
hooks: hooks,
|
|
317
|
+
rules: rules,
|
|
318
|
+
installPath: entry.installPath,
|
|
319
|
+
installedAt: entry.installedAt,
|
|
320
|
+
lastUpdated: entry.lastUpdated,
|
|
321
|
+
gitCommitSha: entry.gitCommitSha,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function getPluginMcpServers(): Record<string, unknown> {
|
|
326
|
+
var data = readJsonFile<InstalledPluginsFile>(INSTALLED_FILE);
|
|
327
|
+
if (!data || !data.plugins) return {};
|
|
328
|
+
var servers: Record<string, unknown> = {};
|
|
329
|
+
var keys = Object.keys(data.plugins);
|
|
330
|
+
for (var k = 0; k < keys.length; k++) {
|
|
331
|
+
var entries = data.plugins[keys[k]];
|
|
332
|
+
for (var e = 0; e < entries.length; e++) {
|
|
333
|
+
var mcpPath = join(entries[e].installPath, ".mcp.json");
|
|
334
|
+
var mcpData = readJsonFile<{ mcpServers?: Record<string, unknown> }>(mcpPath);
|
|
335
|
+
if (mcpData && mcpData.mcpServers) {
|
|
336
|
+
var sKeys = Object.keys(mcpData.mcpServers);
|
|
337
|
+
for (var s = 0; s < sKeys.length; s++) {
|
|
338
|
+
servers[sKeys[s]] = mcpData.mcpServers[sKeys[s]];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return servers;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function getInstalledPluginCount(): number {
|
|
347
|
+
var data = readJsonFile<InstalledPluginsFile>(INSTALLED_FILE);
|
|
348
|
+
if (!data || !data.plugins) return 0;
|
|
349
|
+
return Object.keys(data.plugins).length;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function getPluginSkillRuleTokenEstimate(): number {
|
|
353
|
+
var data = readJsonFile<InstalledPluginsFile>(INSTALLED_FILE);
|
|
354
|
+
if (!data || !data.plugins) return 0;
|
|
355
|
+
var totalChars = 0;
|
|
356
|
+
var keys = Object.keys(data.plugins);
|
|
357
|
+
for (var k = 0; k < keys.length; k++) {
|
|
358
|
+
var entries = data.plugins[keys[k]];
|
|
359
|
+
for (var e = 0; e < entries.length; e++) {
|
|
360
|
+
var skillsDir = join(entries[e].installPath, "skills");
|
|
361
|
+
if (existsSync(skillsDir)) {
|
|
362
|
+
try {
|
|
363
|
+
var dirs = readdirSync(skillsDir, { withFileTypes: true });
|
|
364
|
+
for (var d = 0; d < dirs.length; d++) {
|
|
365
|
+
if (!dirs[d].isDirectory()) continue;
|
|
366
|
+
var skillFile = join(skillsDir, dirs[d].name, "SKILL.md");
|
|
367
|
+
if (existsSync(skillFile)) {
|
|
368
|
+
totalChars += readFileSync(skillFile, "utf-8").length;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} catch {}
|
|
372
|
+
}
|
|
373
|
+
var rulesDir = join(entries[e].installPath, "rules");
|
|
374
|
+
if (existsSync(rulesDir)) {
|
|
375
|
+
try {
|
|
376
|
+
var ruleFiles = readdirSync(rulesDir);
|
|
377
|
+
for (var r = 0; r < ruleFiles.length; r++) {
|
|
378
|
+
if (ruleFiles[r].endsWith(".md")) {
|
|
379
|
+
totalChars += readFileSync(join(rulesDir, ruleFiles[r]), "utf-8").length;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch {}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return Math.round(totalChars / 4);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function whichBinary(name: string): boolean {
|
|
390
|
+
try {
|
|
391
|
+
execSync("which " + JSON.stringify(name), { encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
|
|
392
|
+
return true;
|
|
393
|
+
} catch {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
var LSP_BINARY_MAP: Record<string, string> = {
|
|
399
|
+
"typescript-lsp": "typescript-language-server",
|
|
400
|
+
"pyright-lsp": "pyright-langserver",
|
|
401
|
+
"gopls-lsp": "gopls",
|
|
402
|
+
"rust-analyzer-lsp": "rust-analyzer",
|
|
403
|
+
"clangd-lsp": "clangd",
|
|
404
|
+
"csharp-lsp": "OmniSharp",
|
|
405
|
+
"ruby-lsp": "ruby-lsp",
|
|
406
|
+
"swift-lsp": "sourcekit-lsp",
|
|
407
|
+
"kotlin-lsp": "kotlin-language-server",
|
|
408
|
+
"lua-lsp": "lua-language-server",
|
|
409
|
+
"php-lsp": "phpactor",
|
|
410
|
+
"jdtls-lsp": "jdtls",
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
function getPluginErrors(): PluginError[] {
|
|
414
|
+
var data = readJsonFile<InstalledPluginsFile>(INSTALLED_FILE);
|
|
415
|
+
if (!data || !data.plugins) return [];
|
|
416
|
+
var errors: PluginError[] = [];
|
|
417
|
+
|
|
418
|
+
var keys = Object.keys(data.plugins);
|
|
419
|
+
for (var k = 0; k < keys.length; k++) {
|
|
420
|
+
var key = keys[k];
|
|
421
|
+
var entries = data.plugins[key];
|
|
422
|
+
var atIdx = key.lastIndexOf("@");
|
|
423
|
+
var pluginName = atIdx > 0 ? key.slice(0, atIdx) : key;
|
|
424
|
+
var marketplace = atIdx > 0 ? key.slice(atIdx + 1) : "";
|
|
425
|
+
|
|
426
|
+
for (var e = 0; e < entries.length; e++) {
|
|
427
|
+
var entry = entries[e];
|
|
428
|
+
var errs: string[] = [];
|
|
429
|
+
|
|
430
|
+
if (!existsSync(entry.installPath)) {
|
|
431
|
+
errs.push("Install path does not exist: " + entry.installPath);
|
|
432
|
+
} else {
|
|
433
|
+
try {
|
|
434
|
+
var output = execSync(
|
|
435
|
+
"claude plugin validate " + JSON.stringify(entry.installPath) + " 2>&1",
|
|
436
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
437
|
+
);
|
|
438
|
+
var errorLines = output.split("\n").filter(function (l) { return l.trim().startsWith("❯") || l.trim().startsWith("✘"); });
|
|
439
|
+
for (var el = 0; el < errorLines.length; el++) {
|
|
440
|
+
var line = errorLines[el].trim();
|
|
441
|
+
if (line.startsWith("❯")) {
|
|
442
|
+
errs.push(line.slice(1).trim());
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
} catch (validateErr) {
|
|
446
|
+
var stderr = String(validateErr);
|
|
447
|
+
var stderrLines = stderr.split("\n").filter(function (l) { return l.trim().startsWith("❯"); });
|
|
448
|
+
for (var sl = 0; sl < stderrLines.length; sl++) {
|
|
449
|
+
errs.push(stderrLines[sl].trim().slice(1).trim());
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
var lspBinary = LSP_BINARY_MAP[pluginName];
|
|
454
|
+
if (lspBinary && !whichBinary(lspBinary)) {
|
|
455
|
+
errs.push("Executable not found in $PATH: \"" + lspBinary + "\"");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (errs.length > 0) {
|
|
460
|
+
errors.push({ key: key, name: pluginName, marketplace: marketplace, errors: errs });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
return errors;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function discoverPlugins(): MarketplacePluginEntry[] {
|
|
469
|
+
var marketplaces = readJsonFile<MarketplacesFile>(MARKETPLACES_FILE);
|
|
470
|
+
if (!marketplaces) return [];
|
|
471
|
+
var installCounts = getInstallCounts();
|
|
472
|
+
var installedData = readJsonFile<InstalledPluginsFile>(INSTALLED_FILE);
|
|
473
|
+
var installedKeys = new Set<string>();
|
|
474
|
+
var installedVersions = new Map<string, string>();
|
|
475
|
+
if (installedData && installedData.plugins) {
|
|
476
|
+
var iKeys = Object.keys(installedData.plugins);
|
|
477
|
+
for (var ik = 0; ik < iKeys.length; ik++) {
|
|
478
|
+
installedKeys.add(iKeys[ik]);
|
|
479
|
+
var versions = installedData.plugins[iKeys[ik]];
|
|
480
|
+
if (versions.length > 0) {
|
|
481
|
+
installedVersions.set(iKeys[ik], versions[0].version);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
var results: MarketplacePluginEntry[] = [];
|
|
487
|
+
var mKeys = Object.keys(marketplaces);
|
|
488
|
+
|
|
489
|
+
for (var m = 0; m < mKeys.length; m++) {
|
|
490
|
+
var mName = mKeys[m];
|
|
491
|
+
var mkt = marketplaces[mName];
|
|
492
|
+
var pluginsDir = join(mkt.installLocation, "plugins");
|
|
493
|
+
if (!existsSync(pluginsDir)) continue;
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
var pluginDirs = readdirSync(pluginsDir, { withFileTypes: true });
|
|
497
|
+
for (var p = 0; p < pluginDirs.length; p++) {
|
|
498
|
+
if (!pluginDirs[p].isDirectory()) continue;
|
|
499
|
+
var dirName = pluginDirs[p].name;
|
|
500
|
+
var meta = readPluginJson(join(pluginsDir, dirName));
|
|
501
|
+
var key = dirName + "@" + mName;
|
|
502
|
+
results.push({
|
|
503
|
+
name: dirName,
|
|
504
|
+
marketplace: mName,
|
|
505
|
+
description: meta?.description ?? "",
|
|
506
|
+
author: meta?.author,
|
|
507
|
+
installed: installedKeys.has(key),
|
|
508
|
+
installedVersion: installedVersions.get(key),
|
|
509
|
+
installs: installCounts.get(key),
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
} catch {}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
results.sort(function (a, b) {
|
|
516
|
+
var ai = a.installs ?? 0;
|
|
517
|
+
var bi = b.installs ?? 0;
|
|
518
|
+
return bi - ai;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return results;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
registerHandler("plugin", function (clientId: string, message: ClientMessage) {
|
|
525
|
+
if (message.type === "plugin:list") {
|
|
526
|
+
var plugins = getInstalledPlugins();
|
|
527
|
+
sendTo(clientId, { type: "plugin:list_result", plugins: plugins });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (message.type === "plugin:marketplaces") {
|
|
532
|
+
var marketplaces = getMarketplaces();
|
|
533
|
+
sendTo(clientId, { type: "plugin:marketplaces_result", marketplaces: marketplaces });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (message.type === "plugin:search") {
|
|
538
|
+
var searchMsg = message as { type: "plugin:search"; query: string; marketplace?: string };
|
|
539
|
+
var query = searchMsg.query.trim();
|
|
540
|
+
if (!query) {
|
|
541
|
+
sendTo(clientId, { type: "plugin:search_result", query: query, plugins: [], count: 0 });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
var found = searchMarketplacePlugins(query, searchMsg.marketplace);
|
|
545
|
+
sendTo(clientId, { type: "plugin:search_result", query: query, plugins: found, count: found.length });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (message.type === "plugin:install") {
|
|
550
|
+
var installMsg = message as { type: "plugin:install"; name: string; marketplace: string };
|
|
551
|
+
var installArg = installMsg.name + "@" + installMsg.marketplace;
|
|
552
|
+
try {
|
|
553
|
+
var proc = Bun.spawn(["claude", "plugin", "install", installArg], {
|
|
554
|
+
cwd: homedir(),
|
|
555
|
+
stdout: "pipe",
|
|
556
|
+
stderr: "pipe",
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
var timeout = setTimeout(function () {
|
|
560
|
+
proc.kill();
|
|
561
|
+
}, 120000);
|
|
562
|
+
|
|
563
|
+
void proc.exited.then(function (code) {
|
|
564
|
+
clearTimeout(timeout);
|
|
565
|
+
if (code === 0) {
|
|
566
|
+
sendTo(clientId, { type: "plugin:install_result", success: true, message: "Installed " + installMsg.name + " successfully" });
|
|
567
|
+
sendTo(clientId, { type: "plugin:list_result", plugins: getInstalledPlugins() });
|
|
568
|
+
} else {
|
|
569
|
+
sendTo(clientId, { type: "plugin:install_result", success: false, message: "Install failed (exit code " + code + ")" });
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
} catch (err) {
|
|
573
|
+
sendTo(clientId, { type: "plugin:install_result", success: false, message: "Failed to start install: " + String(err) });
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (message.type === "plugin:uninstall") {
|
|
579
|
+
var uninstallMsg = message as { type: "plugin:uninstall"; name: string; marketplace: string };
|
|
580
|
+
var uninstallArg = uninstallMsg.name + "@" + uninstallMsg.marketplace;
|
|
581
|
+
try {
|
|
582
|
+
var uninstallProc = Bun.spawn(["claude", "plugin", "uninstall", uninstallArg], {
|
|
583
|
+
cwd: homedir(),
|
|
584
|
+
stdout: "pipe",
|
|
585
|
+
stderr: "pipe",
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
var uninstallTimeout = setTimeout(function () {
|
|
589
|
+
uninstallProc.kill();
|
|
590
|
+
}, 60000);
|
|
591
|
+
|
|
592
|
+
void uninstallProc.exited.then(function (code) {
|
|
593
|
+
clearTimeout(uninstallTimeout);
|
|
594
|
+
if (code === 0) {
|
|
595
|
+
sendTo(clientId, { type: "plugin:uninstall_result", success: true, message: "Uninstalled " + uninstallMsg.name + " successfully" });
|
|
596
|
+
sendTo(clientId, { type: "plugin:list_result", plugins: getInstalledPlugins() });
|
|
597
|
+
} else {
|
|
598
|
+
sendTo(clientId, { type: "plugin:uninstall_result", success: false, message: "Uninstall failed (exit code " + code + ")" });
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
} catch (err) {
|
|
602
|
+
sendTo(clientId, { type: "plugin:uninstall_result", success: false, message: "Failed to start uninstall: " + String(err) });
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (message.type === "plugin:update") {
|
|
608
|
+
var updateMsg = message as { type: "plugin:update"; name: string; marketplace: string };
|
|
609
|
+
var updateArg = updateMsg.name + "@" + updateMsg.marketplace;
|
|
610
|
+
try {
|
|
611
|
+
var updateProc = Bun.spawn(["claude", "plugin", "update", updateArg], {
|
|
612
|
+
cwd: homedir(),
|
|
613
|
+
stdout: "pipe",
|
|
614
|
+
stderr: "pipe",
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
var updateTimeout = setTimeout(function () {
|
|
618
|
+
updateProc.kill();
|
|
619
|
+
}, 120000);
|
|
620
|
+
|
|
621
|
+
void updateProc.exited.then(function (code) {
|
|
622
|
+
clearTimeout(updateTimeout);
|
|
623
|
+
if (code === 0) {
|
|
624
|
+
sendTo(clientId, { type: "plugin:update_result", success: true, message: "Updated " + updateMsg.name + " successfully" });
|
|
625
|
+
sendTo(clientId, { type: "plugin:list_result", plugins: getInstalledPlugins() });
|
|
626
|
+
} else {
|
|
627
|
+
sendTo(clientId, { type: "plugin:update_result", success: false, message: "Update failed (exit code " + code + ")" });
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
} catch (err) {
|
|
631
|
+
sendTo(clientId, { type: "plugin:update_result", success: false, message: "Failed to start update: " + String(err) });
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (message.type === "plugin:details") {
|
|
637
|
+
var detailsMsg = message as { type: "plugin:details"; name: string; marketplace: string };
|
|
638
|
+
var details = getPluginDetails(detailsMsg.name, detailsMsg.marketplace);
|
|
639
|
+
if (details) {
|
|
640
|
+
sendTo(clientId, { type: "plugin:details_result", plugin: details });
|
|
641
|
+
} else {
|
|
642
|
+
sendTo(clientId, { type: "plugin:details_result", plugin: null, error: "Plugin not found" });
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (message.type === "plugin:discover") {
|
|
648
|
+
var allPlugins = discoverPlugins();
|
|
649
|
+
sendTo(clientId, { type: "plugin:discover_result", plugins: allPlugins });
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (message.type === "plugin:errors") {
|
|
654
|
+
var pluginErrors = getPluginErrors();
|
|
655
|
+
sendTo(clientId, { type: "plugin:errors_result", errors: pluginErrors });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
});
|
|
@@ -68,6 +68,7 @@ function buildProjectSettings(projectSlug: string): ProjectSettings | { error: s
|
|
|
68
68
|
mcpServers,
|
|
69
69
|
rules,
|
|
70
70
|
skills,
|
|
71
|
+
disabledPlugins: Array.isArray(lattice.disabledPlugins) ? lattice.disabledPlugins as string[] : [],
|
|
71
72
|
global: {
|
|
72
73
|
claudeMd: loadGlobalClaudeMd(),
|
|
73
74
|
defaultModel: "",
|
|
@@ -149,6 +150,11 @@ registerHandler("project-settings", function (clientId: string, message: ClientM
|
|
|
149
150
|
writeProjectMcpServers(project.path, (settings.mcpServers as Record<string, unknown>) ?? {});
|
|
150
151
|
} else if (section === "rules") {
|
|
151
152
|
writeProjectRules(project.path, (settings.rules as Array<{ filename: string; content: string }>) ?? []);
|
|
153
|
+
} else if (section === "plugins") {
|
|
154
|
+
var disabledPlugins = Array.isArray(settings.disabledPlugins) ? settings.disabledPlugins : [];
|
|
155
|
+
mergeProjectClaudeSettings(project.path, {
|
|
156
|
+
lattice: { disabledPlugins: disabledPlugins },
|
|
157
|
+
});
|
|
152
158
|
} else if (section === "permissions") {
|
|
153
159
|
mergeProjectClaudeSettings(project.path, {
|
|
154
160
|
permissions: {
|
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import type { ContextBreakdownSegment } from "@lattice/shared";
|
|
6
6
|
import { guessContextWindow } from "./session";
|
|
7
7
|
import { loadConfig } from "../config";
|
|
8
|
+
import { getInstalledPluginCount, getPluginSkillRuleTokenEstimate } from "../handlers/plugins";
|
|
8
9
|
|
|
9
10
|
var encoder = encodingForModel("gpt-4o");
|
|
10
11
|
|
|
@@ -277,6 +278,17 @@ export async function getContextBreakdown(projectSlug: string, sessionId: string
|
|
|
277
278
|
});
|
|
278
279
|
});
|
|
279
280
|
|
|
281
|
+
var pluginCount = getInstalledPluginCount();
|
|
282
|
+
if (pluginCount > 0) {
|
|
283
|
+
var pluginTokens = getPluginSkillRuleTokenEstimate();
|
|
284
|
+
segments.push({
|
|
285
|
+
label: "Plugins (" + pluginCount + ")",
|
|
286
|
+
tokens: pluginTokens,
|
|
287
|
+
id: "plugins",
|
|
288
|
+
estimated: true,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
280
292
|
segments.push(
|
|
281
293
|
{ label: "Instructions", tokens: instructionsTokens, id: "instructions", estimated: false },
|
|
282
294
|
{ label: "Memory", tokens: memoryTokens, id: "memory", estimated: false },
|