@robota-sdk/agent-sdk 3.0.0-beta.24 → 3.0.0-beta.26
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/dist/node/index.cjs +1013 -21
- package/dist/node/index.d.cts +448 -168
- package/dist/node/index.d.ts +448 -168
- package/dist/node/index.js +1015 -21
- package/package.json +5 -5
package/dist/node/index.cjs
CHANGED
|
@@ -30,8 +30,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AgentExecutor: () => AgentExecutor,
|
|
34
|
+
BundlePluginInstaller: () => BundlePluginInstaller,
|
|
35
|
+
BundlePluginLoader: () => BundlePluginLoader,
|
|
33
36
|
DEFAULT_TOOL_DESCRIPTIONS: () => DEFAULT_TOOL_DESCRIPTIONS,
|
|
34
37
|
FileSessionLogger: () => import_agent_sessions3.FileSessionLogger,
|
|
38
|
+
MarketplaceClient: () => MarketplaceClient,
|
|
39
|
+
PluginSettingsStore: () => PluginSettingsStore,
|
|
40
|
+
PromptExecutor: () => PromptExecutor,
|
|
35
41
|
Session: () => import_agent_sessions2.Session,
|
|
36
42
|
SessionStore: () => import_agent_sessions4.SessionStore,
|
|
37
43
|
SilentSessionLogger: () => import_agent_sessions3.SilentSessionLogger,
|
|
@@ -63,6 +69,113 @@ module.exports = __toCommonJS(index_exports);
|
|
|
63
69
|
// src/types.ts
|
|
64
70
|
var import_agent_core = require("@robota-sdk/agent-core");
|
|
65
71
|
|
|
72
|
+
// src/hooks/prompt-executor.ts
|
|
73
|
+
function extractJson(raw) {
|
|
74
|
+
const codeBlockMatch = /```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/.exec(raw);
|
|
75
|
+
if (codeBlockMatch) {
|
|
76
|
+
return codeBlockMatch[1].trim();
|
|
77
|
+
}
|
|
78
|
+
return raw.trim();
|
|
79
|
+
}
|
|
80
|
+
var PromptExecutor = class {
|
|
81
|
+
type = "prompt";
|
|
82
|
+
providerFactory;
|
|
83
|
+
defaultModel;
|
|
84
|
+
constructor(options) {
|
|
85
|
+
this.providerFactory = options.providerFactory;
|
|
86
|
+
this.defaultModel = options.defaultModel;
|
|
87
|
+
}
|
|
88
|
+
async execute(definition, input) {
|
|
89
|
+
const promptDef = definition;
|
|
90
|
+
const model = promptDef.model ?? this.defaultModel;
|
|
91
|
+
try {
|
|
92
|
+
const provider = this.providerFactory(model);
|
|
93
|
+
const prompt = `${promptDef.prompt}
|
|
94
|
+
|
|
95
|
+
Context:
|
|
96
|
+
${JSON.stringify(input)}
|
|
97
|
+
|
|
98
|
+
Respond with JSON: { "ok": boolean, "reason"?: string }`;
|
|
99
|
+
const rawResponse = await provider.complete(prompt);
|
|
100
|
+
const jsonStr = extractJson(rawResponse);
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(jsonStr);
|
|
104
|
+
} catch {
|
|
105
|
+
return {
|
|
106
|
+
exitCode: 1,
|
|
107
|
+
stdout: "",
|
|
108
|
+
stderr: `Failed to parse AI response as JSON: ${rawResponse}`
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (parsed.ok) {
|
|
112
|
+
return { exitCode: 0, stdout: JSON.stringify(parsed), stderr: "" };
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
exitCode: 2,
|
|
116
|
+
stdout: "",
|
|
117
|
+
stderr: parsed.reason ?? "Blocked by prompt hook"
|
|
118
|
+
};
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
return { exitCode: 1, stdout: "", stderr: message };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/hooks/agent-executor.ts
|
|
127
|
+
var DEFAULT_MAX_TURNS = 50;
|
|
128
|
+
var DEFAULT_TIMEOUT_SECONDS = 60;
|
|
129
|
+
function extractJson2(raw) {
|
|
130
|
+
const codeBlockMatch = /```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/.exec(raw);
|
|
131
|
+
if (codeBlockMatch) {
|
|
132
|
+
return codeBlockMatch[1].trim();
|
|
133
|
+
}
|
|
134
|
+
return raw.trim();
|
|
135
|
+
}
|
|
136
|
+
var AgentExecutor = class {
|
|
137
|
+
type = "agent";
|
|
138
|
+
sessionFactory;
|
|
139
|
+
constructor(options) {
|
|
140
|
+
this.sessionFactory = options.sessionFactory;
|
|
141
|
+
}
|
|
142
|
+
async execute(definition, input) {
|
|
143
|
+
const agentDef = definition;
|
|
144
|
+
const maxTurns = agentDef.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
145
|
+
const timeout = agentDef.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
146
|
+
try {
|
|
147
|
+
const session = this.sessionFactory({ maxTurns, timeout });
|
|
148
|
+
const prompt = `Hook input:
|
|
149
|
+
${JSON.stringify(input)}
|
|
150
|
+
|
|
151
|
+
Respond with JSON: { "ok": boolean, "reason"?: string }`;
|
|
152
|
+
const rawResponse = await session.run(prompt);
|
|
153
|
+
const jsonStr = extractJson2(rawResponse);
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(jsonStr);
|
|
157
|
+
} catch {
|
|
158
|
+
return {
|
|
159
|
+
exitCode: 1,
|
|
160
|
+
stdout: "",
|
|
161
|
+
stderr: `Failed to parse agent response as JSON: ${rawResponse}`
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (parsed.ok) {
|
|
165
|
+
return { exitCode: 0, stdout: JSON.stringify(parsed), stderr: "" };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
exitCode: 2,
|
|
169
|
+
stdout: "",
|
|
170
|
+
stderr: parsed.reason ?? "Blocked by agent hook"
|
|
171
|
+
};
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
174
|
+
return { exitCode: 1, stdout: "", stderr: message };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
66
179
|
// src/assembly/create-session.ts
|
|
67
180
|
var import_agent_sessions = require("@robota-sdk/agent-sessions");
|
|
68
181
|
|
|
@@ -95,6 +208,19 @@ function buildToolsSection(descriptions) {
|
|
|
95
208
|
const lines = ["## Available Tools", ...descriptions.map((d) => `- ${d}`)];
|
|
96
209
|
return lines.join("\n");
|
|
97
210
|
}
|
|
211
|
+
function buildSkillsSection(skills) {
|
|
212
|
+
const invocable = skills.filter((s) => s.disableModelInvocation !== true);
|
|
213
|
+
if (invocable.length === 0) {
|
|
214
|
+
return "";
|
|
215
|
+
}
|
|
216
|
+
const lines = [
|
|
217
|
+
"## Skills",
|
|
218
|
+
"The following skills are available:",
|
|
219
|
+
"",
|
|
220
|
+
...invocable.map((s) => `- ${s.name}: ${s.description}`)
|
|
221
|
+
];
|
|
222
|
+
return lines.join("\n");
|
|
223
|
+
}
|
|
98
224
|
function buildSystemPrompt(params) {
|
|
99
225
|
const { agentsMd, claudeMd, toolDescriptions, trustLevel, projectInfo, cwd, language } = params;
|
|
100
226
|
const sections = [];
|
|
@@ -105,7 +231,9 @@ function buildSystemPrompt(params) {
|
|
|
105
231
|
"Always be precise, follow existing code conventions, and prefer minimal changes."
|
|
106
232
|
];
|
|
107
233
|
if (language) {
|
|
108
|
-
roleLines.push(
|
|
234
|
+
roleLines.push(
|
|
235
|
+
`Always respond in ${language}. Use ${language} for all explanations and communications.`
|
|
236
|
+
);
|
|
109
237
|
}
|
|
110
238
|
sections.push(roleLines.join("\n"));
|
|
111
239
|
if (cwd) {
|
|
@@ -137,6 +265,12 @@ function buildSystemPrompt(params) {
|
|
|
137
265
|
if (toolsSection.length > 0) {
|
|
138
266
|
sections.push(toolsSection);
|
|
139
267
|
}
|
|
268
|
+
if (params.skills !== void 0 && params.skills.length > 0) {
|
|
269
|
+
const skillsSection = buildSkillsSection(params.skills);
|
|
270
|
+
if (skillsSection.length > 0) {
|
|
271
|
+
sections.push(skillsSection);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
140
274
|
return sections.join("\n\n");
|
|
141
275
|
}
|
|
142
276
|
|
|
@@ -203,6 +337,25 @@ function createSession(options) {
|
|
|
203
337
|
allow: [...defaultAllow, ...options.config.permissions.allow ?? []],
|
|
204
338
|
deny: options.config.permissions.deny ?? []
|
|
205
339
|
};
|
|
340
|
+
const hookTypeExecutors = [];
|
|
341
|
+
if (options.providerFactory) {
|
|
342
|
+
hookTypeExecutors.push(
|
|
343
|
+
new PromptExecutor({
|
|
344
|
+
providerFactory: options.providerFactory,
|
|
345
|
+
defaultModel: options.config.provider.model
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (options.sessionFactory) {
|
|
350
|
+
hookTypeExecutors.push(
|
|
351
|
+
new AgentExecutor({
|
|
352
|
+
sessionFactory: options.sessionFactory
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (options.additionalHookExecutors) {
|
|
357
|
+
hookTypeExecutors.push(...options.additionalHookExecutors);
|
|
358
|
+
}
|
|
206
359
|
return new import_agent_sessions.Session({
|
|
207
360
|
tools,
|
|
208
361
|
provider,
|
|
@@ -221,7 +374,8 @@ function createSession(options) {
|
|
|
221
374
|
promptForApproval: options.promptForApproval,
|
|
222
375
|
onCompact: options.onCompact,
|
|
223
376
|
compactInstructions: options.compactInstructions ?? options.context.compactInstructions,
|
|
224
|
-
sessionLogger: options.sessionLogger
|
|
377
|
+
sessionLogger: options.sessionLogger,
|
|
378
|
+
hookTypeExecutors: hookTypeExecutors.length > 0 ? hookTypeExecutors : void 0
|
|
225
379
|
});
|
|
226
380
|
}
|
|
227
381
|
|
|
@@ -248,10 +402,34 @@ var PermissionsSchema = import_zod.z.object({
|
|
|
248
402
|
deny: import_zod.z.array(import_zod.z.string()).optional()
|
|
249
403
|
});
|
|
250
404
|
var EnvSchema = import_zod.z.record(import_zod.z.string()).optional();
|
|
251
|
-
var
|
|
405
|
+
var CommandHookDefinitionSchema = import_zod.z.object({
|
|
252
406
|
type: import_zod.z.literal("command"),
|
|
253
|
-
command: import_zod.z.string()
|
|
407
|
+
command: import_zod.z.string(),
|
|
408
|
+
timeout: import_zod.z.number().optional()
|
|
409
|
+
});
|
|
410
|
+
var HttpHookDefinitionSchema = import_zod.z.object({
|
|
411
|
+
type: import_zod.z.literal("http"),
|
|
412
|
+
url: import_zod.z.string(),
|
|
413
|
+
headers: import_zod.z.record(import_zod.z.string()).optional(),
|
|
414
|
+
timeout: import_zod.z.number().optional()
|
|
415
|
+
});
|
|
416
|
+
var PromptHookDefinitionSchema = import_zod.z.object({
|
|
417
|
+
type: import_zod.z.literal("prompt"),
|
|
418
|
+
prompt: import_zod.z.string(),
|
|
419
|
+
model: import_zod.z.string().optional()
|
|
254
420
|
});
|
|
421
|
+
var AgentHookDefinitionSchema = import_zod.z.object({
|
|
422
|
+
type: import_zod.z.literal("agent"),
|
|
423
|
+
agent: import_zod.z.string(),
|
|
424
|
+
maxTurns: import_zod.z.number().optional(),
|
|
425
|
+
timeout: import_zod.z.number().optional()
|
|
426
|
+
});
|
|
427
|
+
var HookDefinitionSchema = import_zod.z.discriminatedUnion("type", [
|
|
428
|
+
CommandHookDefinitionSchema,
|
|
429
|
+
HttpHookDefinitionSchema,
|
|
430
|
+
PromptHookDefinitionSchema,
|
|
431
|
+
AgentHookDefinitionSchema
|
|
432
|
+
]);
|
|
255
433
|
var HookGroupSchema = import_zod.z.object({
|
|
256
434
|
matcher: import_zod.z.string(),
|
|
257
435
|
hooks: import_zod.z.array(HookDefinitionSchema)
|
|
@@ -260,8 +438,23 @@ var HooksSchema = import_zod.z.object({
|
|
|
260
438
|
PreToolUse: import_zod.z.array(HookGroupSchema).optional(),
|
|
261
439
|
PostToolUse: import_zod.z.array(HookGroupSchema).optional(),
|
|
262
440
|
SessionStart: import_zod.z.array(HookGroupSchema).optional(),
|
|
263
|
-
Stop: import_zod.z.array(HookGroupSchema).optional()
|
|
441
|
+
Stop: import_zod.z.array(HookGroupSchema).optional(),
|
|
442
|
+
PreCompact: import_zod.z.array(HookGroupSchema).optional(),
|
|
443
|
+
PostCompact: import_zod.z.array(HookGroupSchema).optional(),
|
|
444
|
+
UserPromptSubmit: import_zod.z.array(HookGroupSchema).optional(),
|
|
445
|
+
Notification: import_zod.z.array(HookGroupSchema).optional()
|
|
264
446
|
}).optional();
|
|
447
|
+
var EnabledPluginsSchema = import_zod.z.record(import_zod.z.boolean()).optional();
|
|
448
|
+
var MarketplaceSourceSchema = import_zod.z.object({
|
|
449
|
+
source: import_zod.z.object({
|
|
450
|
+
type: import_zod.z.enum(["github", "git", "local", "url"]),
|
|
451
|
+
repo: import_zod.z.string().optional(),
|
|
452
|
+
url: import_zod.z.string().optional(),
|
|
453
|
+
path: import_zod.z.string().optional(),
|
|
454
|
+
ref: import_zod.z.string().optional()
|
|
455
|
+
})
|
|
456
|
+
});
|
|
457
|
+
var ExtraKnownMarketplacesSchema = import_zod.z.record(MarketplaceSourceSchema).optional();
|
|
265
458
|
var SettingsSchema = import_zod.z.object({
|
|
266
459
|
/** Trust level used when no --permission-mode flag is given */
|
|
267
460
|
defaultTrustLevel: import_zod.z.enum(["safe", "moderate", "full"]).optional(),
|
|
@@ -270,7 +463,11 @@ var SettingsSchema = import_zod.z.object({
|
|
|
270
463
|
provider: ProviderSchema.optional(),
|
|
271
464
|
permissions: PermissionsSchema.optional(),
|
|
272
465
|
env: EnvSchema,
|
|
273
|
-
hooks: HooksSchema
|
|
466
|
+
hooks: HooksSchema,
|
|
467
|
+
/** Plugin enablement map: plugin name -> enabled/disabled */
|
|
468
|
+
enabledPlugins: EnabledPluginsSchema,
|
|
469
|
+
/** Extra marketplace URLs for BundlePlugin discovery */
|
|
470
|
+
extraKnownMarketplaces: ExtraKnownMarketplacesSchema
|
|
274
471
|
});
|
|
275
472
|
|
|
276
473
|
// src/config/config-loader.ts
|
|
@@ -337,7 +534,9 @@ function mergeSettings(layers) {
|
|
|
337
534
|
env: {
|
|
338
535
|
...merged.env ?? {},
|
|
339
536
|
...layer.env ?? {}
|
|
340
|
-
}
|
|
537
|
+
},
|
|
538
|
+
enabledPlugins: merged.enabledPlugins !== void 0 || layer.enabledPlugins !== void 0 ? { ...merged.enabledPlugins ?? {}, ...layer.enabledPlugins ?? {} } : void 0,
|
|
539
|
+
extraKnownMarketplaces: layer.extraKnownMarketplaces ?? merged.extraKnownMarketplaces
|
|
341
540
|
};
|
|
342
541
|
}, {});
|
|
343
542
|
}
|
|
@@ -355,25 +554,39 @@ function toResolvedConfig(merged) {
|
|
|
355
554
|
deny: merged.permissions?.deny ?? DEFAULTS.permissions.deny
|
|
356
555
|
},
|
|
357
556
|
env: merged.env ?? DEFAULTS.env,
|
|
358
|
-
hooks: merged.hooks ?? void 0
|
|
557
|
+
hooks: merged.hooks ?? void 0,
|
|
558
|
+
enabledPlugins: merged.enabledPlugins ?? void 0,
|
|
559
|
+
extraKnownMarketplaces: merged.extraKnownMarketplaces ?? void 0
|
|
359
560
|
};
|
|
360
561
|
}
|
|
562
|
+
function getSettingsPaths(cwd) {
|
|
563
|
+
const home = getHomeDir();
|
|
564
|
+
return [
|
|
565
|
+
(0, import_path.join)(home, ".robota", "settings.json"),
|
|
566
|
+
// 1. user (lowest)
|
|
567
|
+
(0, import_path.join)(cwd, ".robota", "settings.json"),
|
|
568
|
+
// 2. project
|
|
569
|
+
(0, import_path.join)(cwd, ".robota", "settings.local.json"),
|
|
570
|
+
// 3. project-local
|
|
571
|
+
(0, import_path.join)(cwd, ".claude", "settings.json"),
|
|
572
|
+
// 4. project, Claude Code compat
|
|
573
|
+
(0, import_path.join)(cwd, ".claude", "settings.local.json")
|
|
574
|
+
// 5. project-local (highest)
|
|
575
|
+
];
|
|
576
|
+
}
|
|
361
577
|
async function loadConfig(cwd) {
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const parsedLayers =
|
|
578
|
+
const allPaths = getSettingsPaths(cwd);
|
|
579
|
+
const rawEntries = [];
|
|
580
|
+
for (const filePath of allPaths) {
|
|
581
|
+
const raw = readJsonFile(filePath);
|
|
582
|
+
if (raw !== void 0) {
|
|
583
|
+
rawEntries.push({ raw, path: filePath });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const parsedLayers = rawEntries.map(({ raw, path }) => {
|
|
371
587
|
const result = SettingsSchema.safeParse(raw);
|
|
372
588
|
if (!result.success) {
|
|
373
|
-
|
|
374
|
-
(_, i) => rawLayers[i] !== void 0
|
|
375
|
-
);
|
|
376
|
-
throw new Error(`Invalid settings in ${paths[index] ?? "unknown"}: ${result.error.message}`);
|
|
589
|
+
throw new Error(`Invalid settings in ${path}: ${result.error.message}`);
|
|
377
590
|
}
|
|
378
591
|
return resolveEnvRefs(result.data);
|
|
379
592
|
});
|
|
@@ -587,6 +800,779 @@ function userPaths() {
|
|
|
587
800
|
};
|
|
588
801
|
}
|
|
589
802
|
|
|
803
|
+
// src/plugins/plugin-settings-store.ts
|
|
804
|
+
var import_node_fs = require("fs");
|
|
805
|
+
var import_node_path2 = require("path");
|
|
806
|
+
var PluginSettingsStore = class {
|
|
807
|
+
settingsPath;
|
|
808
|
+
constructor(settingsPath) {
|
|
809
|
+
this.settingsPath = settingsPath;
|
|
810
|
+
}
|
|
811
|
+
/** Read the full settings file from disk. */
|
|
812
|
+
readAll() {
|
|
813
|
+
if (!(0, import_node_fs.existsSync)(this.settingsPath)) {
|
|
814
|
+
return {};
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
const raw = (0, import_node_fs.readFileSync)(this.settingsPath, "utf-8");
|
|
818
|
+
const data = JSON.parse(raw);
|
|
819
|
+
if (typeof data === "object" && data !== null) {
|
|
820
|
+
return data;
|
|
821
|
+
}
|
|
822
|
+
return {};
|
|
823
|
+
} catch {
|
|
824
|
+
return {};
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/** Write the full settings file to disk. */
|
|
828
|
+
writeAll(settings) {
|
|
829
|
+
const dir = (0, import_node_path2.dirname)(this.settingsPath);
|
|
830
|
+
if (!(0, import_node_fs.existsSync)(dir)) {
|
|
831
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
832
|
+
}
|
|
833
|
+
(0, import_node_fs.writeFileSync)(this.settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
834
|
+
}
|
|
835
|
+
// --- enabledPlugins ---
|
|
836
|
+
/** Get the enabledPlugins map. */
|
|
837
|
+
getEnabledPlugins() {
|
|
838
|
+
const settings = this.readAll();
|
|
839
|
+
const ep = settings.enabledPlugins;
|
|
840
|
+
if (typeof ep === "object" && ep !== null) {
|
|
841
|
+
return ep;
|
|
842
|
+
}
|
|
843
|
+
return {};
|
|
844
|
+
}
|
|
845
|
+
/** Set a single plugin's enabled state. */
|
|
846
|
+
setPluginEnabled(pluginId, enabled) {
|
|
847
|
+
const settings = this.readAll();
|
|
848
|
+
const ep = this.getEnabledPluginsFrom(settings);
|
|
849
|
+
ep[pluginId] = enabled;
|
|
850
|
+
settings.enabledPlugins = ep;
|
|
851
|
+
this.writeAll(settings);
|
|
852
|
+
}
|
|
853
|
+
/** Remove a plugin from enabledPlugins. */
|
|
854
|
+
removePluginEntry(pluginId) {
|
|
855
|
+
const settings = this.readAll();
|
|
856
|
+
const ep = this.getEnabledPluginsFrom(settings);
|
|
857
|
+
delete ep[pluginId];
|
|
858
|
+
settings.enabledPlugins = ep;
|
|
859
|
+
this.writeAll(settings);
|
|
860
|
+
}
|
|
861
|
+
// --- extraKnownMarketplaces ---
|
|
862
|
+
/** Get all persisted marketplace sources. */
|
|
863
|
+
getMarketplaceSources() {
|
|
864
|
+
const settings = this.readAll();
|
|
865
|
+
const extra = settings.extraKnownMarketplaces;
|
|
866
|
+
if (typeof extra === "object" && extra !== null) {
|
|
867
|
+
return extra;
|
|
868
|
+
}
|
|
869
|
+
return {};
|
|
870
|
+
}
|
|
871
|
+
/** Add or update a marketplace source. */
|
|
872
|
+
setMarketplaceSource(name, source) {
|
|
873
|
+
const settings = this.readAll();
|
|
874
|
+
const extra = this.getMarketplaceSourcesFrom(settings);
|
|
875
|
+
extra[name] = { source };
|
|
876
|
+
settings.extraKnownMarketplaces = extra;
|
|
877
|
+
this.writeAll(settings);
|
|
878
|
+
}
|
|
879
|
+
/** Remove a marketplace source. */
|
|
880
|
+
removeMarketplaceSource(name) {
|
|
881
|
+
const settings = this.readAll();
|
|
882
|
+
const extra = this.getMarketplaceSourcesFrom(settings);
|
|
883
|
+
delete extra[name];
|
|
884
|
+
settings.extraKnownMarketplaces = extra;
|
|
885
|
+
this.writeAll(settings);
|
|
886
|
+
}
|
|
887
|
+
// --- helpers ---
|
|
888
|
+
getEnabledPluginsFrom(settings) {
|
|
889
|
+
const ep = settings.enabledPlugins;
|
|
890
|
+
if (typeof ep === "object" && ep !== null) {
|
|
891
|
+
return ep;
|
|
892
|
+
}
|
|
893
|
+
return {};
|
|
894
|
+
}
|
|
895
|
+
getMarketplaceSourcesFrom(settings) {
|
|
896
|
+
const extra = settings.extraKnownMarketplaces;
|
|
897
|
+
if (typeof extra === "object" && extra !== null) {
|
|
898
|
+
return extra;
|
|
899
|
+
}
|
|
900
|
+
return {};
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// src/plugins/bundle-plugin-loader.ts
|
|
905
|
+
var import_node_fs2 = require("fs");
|
|
906
|
+
var import_node_path3 = require("path");
|
|
907
|
+
function parseSkillFrontmatter(raw) {
|
|
908
|
+
const trimmed = raw.trimStart();
|
|
909
|
+
if (!trimmed.startsWith("---")) {
|
|
910
|
+
return { metadata: {}, content: raw };
|
|
911
|
+
}
|
|
912
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
913
|
+
if (endIndex === -1) {
|
|
914
|
+
return { metadata: {}, content: raw };
|
|
915
|
+
}
|
|
916
|
+
const frontmatterBlock = trimmed.slice(3, endIndex).trim();
|
|
917
|
+
const content = trimmed.slice(endIndex + 3).trimStart();
|
|
918
|
+
const metadata = {};
|
|
919
|
+
for (const line of frontmatterBlock.split("\n")) {
|
|
920
|
+
const colonIndex = line.indexOf(":");
|
|
921
|
+
if (colonIndex === -1) continue;
|
|
922
|
+
const key = line.slice(0, colonIndex).trim();
|
|
923
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
924
|
+
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
|
|
925
|
+
const inner = value.slice(1, -1);
|
|
926
|
+
value = inner.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
927
|
+
}
|
|
928
|
+
if (key) {
|
|
929
|
+
metadata[key] = value;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return { metadata, content };
|
|
933
|
+
}
|
|
934
|
+
function validateManifest(data) {
|
|
935
|
+
if (typeof data !== "object" || data === null) return null;
|
|
936
|
+
const obj = data;
|
|
937
|
+
if (typeof obj.name !== "string") return null;
|
|
938
|
+
if (typeof obj.version !== "string") return null;
|
|
939
|
+
if (typeof obj.description !== "string") return null;
|
|
940
|
+
const features = typeof obj.features === "object" && obj.features !== null ? obj.features : {};
|
|
941
|
+
return {
|
|
942
|
+
name: obj.name,
|
|
943
|
+
version: obj.version,
|
|
944
|
+
description: obj.description,
|
|
945
|
+
features: {
|
|
946
|
+
commands: features.commands === true ? true : void 0,
|
|
947
|
+
agents: features.agents === true ? true : void 0,
|
|
948
|
+
skills: features.skills === true ? true : void 0,
|
|
949
|
+
hooks: features.hooks === true ? true : void 0,
|
|
950
|
+
mcp: features.mcp === true ? true : void 0
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
function getSortedSubdirs(dirPath) {
|
|
955
|
+
if (!(0, import_node_fs2.existsSync)(dirPath)) return [];
|
|
956
|
+
try {
|
|
957
|
+
const entries = (0, import_node_fs2.readdirSync)(dirPath, { withFileTypes: true });
|
|
958
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
959
|
+
} catch {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
var BundlePluginLoader = class {
|
|
964
|
+
pluginsDir;
|
|
965
|
+
enabledPlugins;
|
|
966
|
+
constructor(pluginsDir, enabledPlugins) {
|
|
967
|
+
this.pluginsDir = pluginsDir;
|
|
968
|
+
this.enabledPlugins = enabledPlugins ?? {};
|
|
969
|
+
}
|
|
970
|
+
/** Load all discovered and enabled bundle plugins (sync). */
|
|
971
|
+
loadPluginsSync() {
|
|
972
|
+
return this.discoverAndLoad();
|
|
973
|
+
}
|
|
974
|
+
/** Load all discovered and enabled bundle plugins (async wrapper). */
|
|
975
|
+
async loadAll() {
|
|
976
|
+
return this.discoverAndLoad();
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Discover and load plugins from the cache directory.
|
|
980
|
+
*
|
|
981
|
+
* Directory structure: `<pluginsDir>/cache/<marketplace>/<plugin>/<version>/`
|
|
982
|
+
* For each marketplace/plugin pair, the latest version (lexicographically last) is loaded.
|
|
983
|
+
*/
|
|
984
|
+
discoverAndLoad() {
|
|
985
|
+
const cacheDir = (0, import_node_path3.join)(this.pluginsDir, "cache");
|
|
986
|
+
if (!(0, import_node_fs2.existsSync)(cacheDir)) {
|
|
987
|
+
return [];
|
|
988
|
+
}
|
|
989
|
+
const results = [];
|
|
990
|
+
const marketplaces = getSortedSubdirs(cacheDir);
|
|
991
|
+
for (const marketplace of marketplaces) {
|
|
992
|
+
const marketplaceDir = (0, import_node_path3.join)(cacheDir, marketplace);
|
|
993
|
+
const plugins = getSortedSubdirs(marketplaceDir);
|
|
994
|
+
for (const pluginName of plugins) {
|
|
995
|
+
const pluginDir = (0, import_node_path3.join)(marketplaceDir, pluginName);
|
|
996
|
+
const versions = getSortedSubdirs(pluginDir);
|
|
997
|
+
if (versions.length === 0) continue;
|
|
998
|
+
const latestVersion = versions[versions.length - 1];
|
|
999
|
+
const versionDir = (0, import_node_path3.join)(pluginDir, latestVersion);
|
|
1000
|
+
const manifestPath = (0, import_node_path3.join)(versionDir, ".claude-plugin", "plugin.json");
|
|
1001
|
+
if (!(0, import_node_fs2.existsSync)(manifestPath)) continue;
|
|
1002
|
+
const manifest = this.readManifest(manifestPath);
|
|
1003
|
+
if (!manifest) continue;
|
|
1004
|
+
const pluginId = `${manifest.name}@${marketplace}`;
|
|
1005
|
+
if (this.isDisabled(pluginId, manifest.name)) continue;
|
|
1006
|
+
const loaded = this.loadPlugin(versionDir, manifest);
|
|
1007
|
+
results.push(loaded);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return results;
|
|
1011
|
+
}
|
|
1012
|
+
/** Read and validate a plugin.json manifest. Returns null on failure. */
|
|
1013
|
+
readManifest(path) {
|
|
1014
|
+
try {
|
|
1015
|
+
const raw = (0, import_node_fs2.readFileSync)(path, "utf-8");
|
|
1016
|
+
const data = JSON.parse(raw);
|
|
1017
|
+
return validateManifest(data);
|
|
1018
|
+
} catch {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Check if a plugin is explicitly disabled.
|
|
1024
|
+
* Checks both `name@marketplace` and `name` keys.
|
|
1025
|
+
* Plugins not listed in enabledPlugins are enabled by default.
|
|
1026
|
+
*/
|
|
1027
|
+
isDisabled(pluginId, pluginName) {
|
|
1028
|
+
if (pluginId in this.enabledPlugins) {
|
|
1029
|
+
return this.enabledPlugins[pluginId] === false;
|
|
1030
|
+
}
|
|
1031
|
+
if (pluginName in this.enabledPlugins) {
|
|
1032
|
+
return this.enabledPlugins[pluginName] === false;
|
|
1033
|
+
}
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
/** Load a single plugin's skills, hooks, agents, and MCP config. */
|
|
1037
|
+
loadPlugin(pluginDir, manifest) {
|
|
1038
|
+
return {
|
|
1039
|
+
manifest,
|
|
1040
|
+
skills: this.loadSkills(pluginDir, manifest.name),
|
|
1041
|
+
commands: this.loadCommands(pluginDir, manifest.name),
|
|
1042
|
+
hooks: this.loadHooks(pluginDir),
|
|
1043
|
+
mcpConfig: this.loadMcpConfig(pluginDir),
|
|
1044
|
+
agents: this.loadAgents(pluginDir),
|
|
1045
|
+
pluginDir
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
/** Load skills from the plugin's skills/ directory. */
|
|
1049
|
+
loadSkills(pluginDir, pluginName) {
|
|
1050
|
+
const skillsDir = (0, import_node_path3.join)(pluginDir, "skills");
|
|
1051
|
+
if (!(0, import_node_fs2.existsSync)(skillsDir)) return [];
|
|
1052
|
+
const entries = (0, import_node_fs2.readdirSync)(skillsDir, { withFileTypes: true });
|
|
1053
|
+
const skills = [];
|
|
1054
|
+
for (const entry of entries) {
|
|
1055
|
+
if (!entry.isDirectory()) continue;
|
|
1056
|
+
const skillFile = (0, import_node_path3.join)(skillsDir, entry.name, "SKILL.md");
|
|
1057
|
+
if (!(0, import_node_fs2.existsSync)(skillFile)) continue;
|
|
1058
|
+
const raw = (0, import_node_fs2.readFileSync)(skillFile, "utf-8");
|
|
1059
|
+
const { metadata, content } = parseSkillFrontmatter(raw);
|
|
1060
|
+
const description = typeof metadata.description === "string" ? metadata.description : "";
|
|
1061
|
+
const skill = {
|
|
1062
|
+
name: entry.name,
|
|
1063
|
+
description,
|
|
1064
|
+
skillContent: content,
|
|
1065
|
+
...metadata
|
|
1066
|
+
};
|
|
1067
|
+
skills.push(skill);
|
|
1068
|
+
}
|
|
1069
|
+
return skills;
|
|
1070
|
+
}
|
|
1071
|
+
/** Load commands from the plugin's commands/ directory (flat .md files). */
|
|
1072
|
+
loadCommands(pluginDir, pluginName) {
|
|
1073
|
+
const commandsDir = (0, import_node_path3.join)(pluginDir, "commands");
|
|
1074
|
+
if (!(0, import_node_fs2.existsSync)(commandsDir)) return [];
|
|
1075
|
+
const entries = (0, import_node_fs2.readdirSync)(commandsDir, { withFileTypes: true });
|
|
1076
|
+
const commands = [];
|
|
1077
|
+
for (const entry of entries) {
|
|
1078
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1079
|
+
const raw = (0, import_node_fs2.readFileSync)((0, import_node_path3.join)(commandsDir, entry.name), "utf-8");
|
|
1080
|
+
const { metadata, content } = parseSkillFrontmatter(raw);
|
|
1081
|
+
const name = typeof metadata.name === "string" ? metadata.name : entry.name.replace(/\.md$/, "");
|
|
1082
|
+
const description = typeof metadata.description === "string" ? metadata.description : "";
|
|
1083
|
+
commands.push({
|
|
1084
|
+
...metadata,
|
|
1085
|
+
name: `${pluginName}:${name}`,
|
|
1086
|
+
description,
|
|
1087
|
+
skillContent: content
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
return commands;
|
|
1091
|
+
}
|
|
1092
|
+
/** Load hooks from hooks/hooks.json if present. */
|
|
1093
|
+
loadHooks(pluginDir) {
|
|
1094
|
+
const hooksPath = (0, import_node_path3.join)(pluginDir, "hooks", "hooks.json");
|
|
1095
|
+
if (!(0, import_node_fs2.existsSync)(hooksPath)) return {};
|
|
1096
|
+
try {
|
|
1097
|
+
const raw = (0, import_node_fs2.readFileSync)(hooksPath, "utf-8");
|
|
1098
|
+
const data = JSON.parse(raw);
|
|
1099
|
+
if (typeof data === "object" && data !== null) {
|
|
1100
|
+
return data;
|
|
1101
|
+
}
|
|
1102
|
+
return {};
|
|
1103
|
+
} catch {
|
|
1104
|
+
return {};
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
/** Load MCP server configuration if present. Checks `.mcp.json` at plugin root first. */
|
|
1108
|
+
loadMcpConfig(pluginDir) {
|
|
1109
|
+
const primaryPath = (0, import_node_path3.join)(pluginDir, ".mcp.json");
|
|
1110
|
+
const fallbackPath = (0, import_node_path3.join)(pluginDir, ".claude-plugin", "mcp.json");
|
|
1111
|
+
const mcpPath = (0, import_node_fs2.existsSync)(primaryPath) ? primaryPath : fallbackPath;
|
|
1112
|
+
if (!(0, import_node_fs2.existsSync)(mcpPath)) return void 0;
|
|
1113
|
+
try {
|
|
1114
|
+
const raw = (0, import_node_fs2.readFileSync)(mcpPath, "utf-8");
|
|
1115
|
+
return JSON.parse(raw);
|
|
1116
|
+
} catch {
|
|
1117
|
+
return void 0;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/** Load agent definitions from agents/ directory if present. */
|
|
1121
|
+
loadAgents(pluginDir) {
|
|
1122
|
+
const agentsDir = (0, import_node_path3.join)(pluginDir, "agents");
|
|
1123
|
+
if (!(0, import_node_fs2.existsSync)(agentsDir)) return [];
|
|
1124
|
+
try {
|
|
1125
|
+
const entries = (0, import_node_fs2.readdirSync)(agentsDir, { withFileTypes: true });
|
|
1126
|
+
return entries.filter((e) => e.isDirectory() || e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, ""));
|
|
1127
|
+
} catch {
|
|
1128
|
+
return [];
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// src/plugins/bundle-plugin-installer.ts
|
|
1134
|
+
var import_node_child_process = require("child_process");
|
|
1135
|
+
var import_node_fs3 = require("fs");
|
|
1136
|
+
var import_node_path4 = require("path");
|
|
1137
|
+
var GIT_CLONE_TIMEOUT_MS = 6e4;
|
|
1138
|
+
var BundlePluginInstaller = class {
|
|
1139
|
+
pluginsDir;
|
|
1140
|
+
cacheDir;
|
|
1141
|
+
registryPath;
|
|
1142
|
+
settingsStore;
|
|
1143
|
+
marketplaceClient;
|
|
1144
|
+
exec;
|
|
1145
|
+
constructor(options) {
|
|
1146
|
+
this.pluginsDir = options.pluginsDir;
|
|
1147
|
+
this.cacheDir = (0, import_node_path4.join)(this.pluginsDir, "cache");
|
|
1148
|
+
this.registryPath = (0, import_node_path4.join)(this.pluginsDir, "installed_plugins.json");
|
|
1149
|
+
this.settingsStore = options.settingsStore;
|
|
1150
|
+
this.marketplaceClient = options.marketplaceClient;
|
|
1151
|
+
this.exec = options.exec ?? this.defaultExec;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Install a plugin from a marketplace.
|
|
1155
|
+
*
|
|
1156
|
+
* 1. Read marketplace manifest to find the plugin entry.
|
|
1157
|
+
* 2. Resolve source (relative path, github, or url).
|
|
1158
|
+
* 3. Copy/clone to `cache/<marketplace>/<plugin>/<version>/`.
|
|
1159
|
+
* 4. Record in `installed_plugins.json`.
|
|
1160
|
+
*/
|
|
1161
|
+
async install(pluginName, marketplaceName) {
|
|
1162
|
+
const manifest = this.marketplaceClient.fetchManifest(marketplaceName);
|
|
1163
|
+
const entry = manifest.plugins.find((p) => p.name === pluginName);
|
|
1164
|
+
if (!entry) {
|
|
1165
|
+
throw new Error(`Plugin "${pluginName}" not found in marketplace "${marketplaceName}"`);
|
|
1166
|
+
}
|
|
1167
|
+
const version = this.resolveVersion(entry, marketplaceName);
|
|
1168
|
+
const targetDir = (0, import_node_path4.join)(this.cacheDir, marketplaceName, pluginName, version);
|
|
1169
|
+
if ((0, import_node_fs3.existsSync)(targetDir)) {
|
|
1170
|
+
throw new Error(
|
|
1171
|
+
`Plugin "${pluginName}" version "${version}" is already installed from "${marketplaceName}"`
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
this.resolveAndInstall(entry.source, marketplaceName, pluginName, targetDir);
|
|
1175
|
+
const pluginId = `${pluginName}@${marketplaceName}`;
|
|
1176
|
+
const registry = this.readRegistry();
|
|
1177
|
+
registry[pluginId] = {
|
|
1178
|
+
pluginName,
|
|
1179
|
+
marketplace: marketplaceName,
|
|
1180
|
+
version,
|
|
1181
|
+
installPath: targetDir,
|
|
1182
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1183
|
+
};
|
|
1184
|
+
this.writeRegistry(registry);
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Uninstall a plugin.
|
|
1188
|
+
* Removes from cache and from installed_plugins.json.
|
|
1189
|
+
*/
|
|
1190
|
+
async uninstall(pluginId) {
|
|
1191
|
+
const registry = this.readRegistry();
|
|
1192
|
+
const record = registry[pluginId];
|
|
1193
|
+
if (!record) {
|
|
1194
|
+
throw new Error(`Plugin "${pluginId}" is not installed`);
|
|
1195
|
+
}
|
|
1196
|
+
if ((0, import_node_fs3.existsSync)(record.installPath)) {
|
|
1197
|
+
(0, import_node_fs3.rmSync)(record.installPath, { recursive: true, force: true });
|
|
1198
|
+
}
|
|
1199
|
+
delete registry[pluginId];
|
|
1200
|
+
this.writeRegistry(registry);
|
|
1201
|
+
this.settingsStore.removePluginEntry(pluginId);
|
|
1202
|
+
}
|
|
1203
|
+
/** Enable a plugin by setting its enabledPlugins entry to true. */
|
|
1204
|
+
async enable(pluginId) {
|
|
1205
|
+
this.settingsStore.setPluginEnabled(pluginId, true);
|
|
1206
|
+
}
|
|
1207
|
+
/** Disable a plugin by setting its enabledPlugins entry to false. */
|
|
1208
|
+
async disable(pluginId) {
|
|
1209
|
+
this.settingsStore.setPluginEnabled(pluginId, false);
|
|
1210
|
+
}
|
|
1211
|
+
/** Get all installed plugins. */
|
|
1212
|
+
getInstalledPlugins() {
|
|
1213
|
+
return this.readRegistry();
|
|
1214
|
+
}
|
|
1215
|
+
/** Get plugins installed from a specific marketplace. */
|
|
1216
|
+
getPluginsByMarketplace(marketplaceName) {
|
|
1217
|
+
const registry = this.readRegistry();
|
|
1218
|
+
return Object.values(registry).filter((r) => r.marketplace === marketplaceName);
|
|
1219
|
+
}
|
|
1220
|
+
// --- Private helpers ---
|
|
1221
|
+
/** Resolve the version for a plugin entry. */
|
|
1222
|
+
resolveVersion(entry, marketplaceName) {
|
|
1223
|
+
const entryWithVersion = entry;
|
|
1224
|
+
if (typeof entryWithVersion.version === "string" && entryWithVersion.version) {
|
|
1225
|
+
return entryWithVersion.version;
|
|
1226
|
+
}
|
|
1227
|
+
return this.marketplaceClient.getMarketplaceSha(marketplaceName);
|
|
1228
|
+
}
|
|
1229
|
+
/** Resolve the source and install the plugin. */
|
|
1230
|
+
resolveAndInstall(source, marketplaceName, pluginName, targetDir) {
|
|
1231
|
+
(0, import_node_fs3.mkdirSync)(targetDir, { recursive: true });
|
|
1232
|
+
if (typeof source === "string") {
|
|
1233
|
+
const marketplaceDir = this.marketplaceClient.getMarketplaceDir(marketplaceName);
|
|
1234
|
+
const sourcePath = (0, import_node_path4.join)(marketplaceDir, source);
|
|
1235
|
+
if (!(0, import_node_fs3.existsSync)(sourcePath)) {
|
|
1236
|
+
(0, import_node_fs3.rmSync)(targetDir, { recursive: true, force: true });
|
|
1237
|
+
throw new Error(
|
|
1238
|
+
`Plugin source path "${source}" not found in marketplace "${marketplaceName}"`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
(0, import_node_fs3.cpSync)(sourcePath, targetDir, { recursive: true });
|
|
1242
|
+
} else if (source.type === "github") {
|
|
1243
|
+
const repoUrl = `https://github.com/${source.repo}.git`;
|
|
1244
|
+
this.cloneToDir(repoUrl, targetDir, pluginName);
|
|
1245
|
+
} else if (source.type === "url") {
|
|
1246
|
+
(0, import_node_fs3.rmSync)(targetDir, { recursive: true, force: true });
|
|
1247
|
+
throw new Error("URL source installation is not yet supported");
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
/** Clone a git repository to the target directory. */
|
|
1251
|
+
cloneToDir(repoUrl, targetDir, pluginName) {
|
|
1252
|
+
(0, import_node_fs3.rmSync)(targetDir, { recursive: true, force: true });
|
|
1253
|
+
const command = `git clone --depth 1 ${repoUrl} ${targetDir}`;
|
|
1254
|
+
try {
|
|
1255
|
+
this.exec(command, { timeout: GIT_CLONE_TIMEOUT_MS, stdio: "pipe" });
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1258
|
+
throw new Error(`Failed to clone plugin "${pluginName}": ${message}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/** Read the installed_plugins.json registry. */
|
|
1262
|
+
readRegistry() {
|
|
1263
|
+
if (!(0, import_node_fs3.existsSync)(this.registryPath)) {
|
|
1264
|
+
return {};
|
|
1265
|
+
}
|
|
1266
|
+
try {
|
|
1267
|
+
const raw = (0, import_node_fs3.readFileSync)(this.registryPath, "utf-8");
|
|
1268
|
+
const data = JSON.parse(raw);
|
|
1269
|
+
if (typeof data === "object" && data !== null) {
|
|
1270
|
+
return data;
|
|
1271
|
+
}
|
|
1272
|
+
return {};
|
|
1273
|
+
} catch {
|
|
1274
|
+
return {};
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
/** Write the installed_plugins.json registry. */
|
|
1278
|
+
writeRegistry(registry) {
|
|
1279
|
+
const dir = (0, import_node_path4.dirname)(this.registryPath);
|
|
1280
|
+
if (!(0, import_node_fs3.existsSync)(dir)) {
|
|
1281
|
+
(0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
1282
|
+
}
|
|
1283
|
+
(0, import_node_fs3.writeFileSync)(this.registryPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1284
|
+
}
|
|
1285
|
+
/** Default exec implementation using child_process. */
|
|
1286
|
+
defaultExec(command, options) {
|
|
1287
|
+
return (0, import_node_child_process.execSync)(command, { timeout: options.timeout, stdio: "pipe" });
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
// src/plugins/marketplace-client.ts
|
|
1292
|
+
var import_node_child_process2 = require("child_process");
|
|
1293
|
+
var import_node_fs4 = require("fs");
|
|
1294
|
+
var import_node_path5 = require("path");
|
|
1295
|
+
var GIT_TIMEOUT_MS = 6e4;
|
|
1296
|
+
var MarketplaceClient = class {
|
|
1297
|
+
pluginsDir;
|
|
1298
|
+
exec;
|
|
1299
|
+
marketplacesDir;
|
|
1300
|
+
registryPath;
|
|
1301
|
+
constructor(options) {
|
|
1302
|
+
this.pluginsDir = options.pluginsDir;
|
|
1303
|
+
this.exec = options.exec ?? this.defaultExec;
|
|
1304
|
+
this.marketplacesDir = (0, import_node_path5.join)(this.pluginsDir, "marketplaces");
|
|
1305
|
+
this.registryPath = (0, import_node_path5.join)(this.pluginsDir, "known_marketplaces.json");
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Add a marketplace by cloning its repository.
|
|
1309
|
+
*
|
|
1310
|
+
* 1. Parse source: `owner/repo` string becomes a GitHub source.
|
|
1311
|
+
* 2. Shallow git clone (`--depth 1`) to `marketplaces/<name>/`.
|
|
1312
|
+
* 3. Read `.claude-plugin/marketplace.json` for the `name` field.
|
|
1313
|
+
* 4. Register in `known_marketplaces.json`.
|
|
1314
|
+
*
|
|
1315
|
+
* Returns the registered marketplace name from the manifest.
|
|
1316
|
+
*/
|
|
1317
|
+
addMarketplace(source) {
|
|
1318
|
+
const tempName = "temp-" + Date.now().toString(36);
|
|
1319
|
+
const tempDir = (0, import_node_path5.join)(this.marketplacesDir, tempName);
|
|
1320
|
+
(0, import_node_fs4.mkdirSync)(this.marketplacesDir, { recursive: true });
|
|
1321
|
+
if (source.type === "local") {
|
|
1322
|
+
if (!(0, import_node_fs4.existsSync)(source.path)) {
|
|
1323
|
+
throw new Error(`Local marketplace path does not exist: ${source.path}`);
|
|
1324
|
+
}
|
|
1325
|
+
(0, import_node_fs4.cpSync)(source.path, tempDir, { recursive: true });
|
|
1326
|
+
} else {
|
|
1327
|
+
const cloneUrl = this.resolveCloneUrl(source);
|
|
1328
|
+
const command = `git clone --depth 1 ${cloneUrl} ${tempDir}`;
|
|
1329
|
+
try {
|
|
1330
|
+
this.exec(command, { timeout: GIT_TIMEOUT_MS, stdio: "pipe" });
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1333
|
+
throw new Error(`Failed to clone marketplace: ${message}`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const manifestPath = (0, import_node_path5.join)(tempDir, ".claude-plugin", "marketplace.json");
|
|
1337
|
+
if (!(0, import_node_fs4.existsSync)(manifestPath)) {
|
|
1338
|
+
(0, import_node_fs4.rmSync)(tempDir, { recursive: true, force: true });
|
|
1339
|
+
throw new Error(
|
|
1340
|
+
source.type === "local" ? "Local directory does not contain .claude-plugin/marketplace.json" : "Cloned repository does not contain .claude-plugin/marketplace.json"
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
const manifest = this.readManifestFromPath(manifestPath);
|
|
1344
|
+
const name = manifest.name;
|
|
1345
|
+
if (!name) {
|
|
1346
|
+
(0, import_node_fs4.rmSync)(tempDir, { recursive: true, force: true });
|
|
1347
|
+
throw new Error('Marketplace manifest does not contain a "name" field');
|
|
1348
|
+
}
|
|
1349
|
+
const registry = this.readRegistry();
|
|
1350
|
+
if (registry[name]) {
|
|
1351
|
+
(0, import_node_fs4.rmSync)(tempDir, { recursive: true, force: true });
|
|
1352
|
+
throw new Error(`Marketplace "${name}" already exists`);
|
|
1353
|
+
}
|
|
1354
|
+
const finalDir = (0, import_node_path5.join)(this.marketplacesDir, name);
|
|
1355
|
+
(0, import_node_fs4.renameSync)(tempDir, finalDir);
|
|
1356
|
+
registry[name] = {
|
|
1357
|
+
source,
|
|
1358
|
+
installLocation: finalDir,
|
|
1359
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1360
|
+
};
|
|
1361
|
+
this.writeRegistry(registry);
|
|
1362
|
+
return name;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* Remove a marketplace.
|
|
1366
|
+
* Uninstalls all plugins from that marketplace, then deletes the clone directory
|
|
1367
|
+
* and removes from the registry.
|
|
1368
|
+
*/
|
|
1369
|
+
removeMarketplace(name) {
|
|
1370
|
+
const registry = this.readRegistry();
|
|
1371
|
+
const entry = registry[name];
|
|
1372
|
+
if (!entry) {
|
|
1373
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1374
|
+
}
|
|
1375
|
+
this.removeInstalledPluginsForMarketplace(name);
|
|
1376
|
+
if ((0, import_node_fs4.existsSync)(entry.installLocation)) {
|
|
1377
|
+
(0, import_node_fs4.rmSync)(entry.installLocation, { recursive: true, force: true });
|
|
1378
|
+
}
|
|
1379
|
+
delete registry[name];
|
|
1380
|
+
this.writeRegistry(registry);
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Update a marketplace by running git pull on its clone.
|
|
1384
|
+
* The manifest is re-read from disk on demand (via fetchManifest), so the
|
|
1385
|
+
* updated manifest is automatically available after pull.
|
|
1386
|
+
*
|
|
1387
|
+
* TODO: After pull, detect version changes in installed plugins and offer
|
|
1388
|
+
* to update them (re-install at new version).
|
|
1389
|
+
*/
|
|
1390
|
+
updateMarketplace(name) {
|
|
1391
|
+
const registry = this.readRegistry();
|
|
1392
|
+
const entry = registry[name];
|
|
1393
|
+
if (!entry) {
|
|
1394
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1395
|
+
}
|
|
1396
|
+
if (!(0, import_node_fs4.existsSync)(entry.installLocation)) {
|
|
1397
|
+
throw new Error(`Marketplace directory for "${name}" does not exist`);
|
|
1398
|
+
}
|
|
1399
|
+
if (entry.source.type === "local") {
|
|
1400
|
+
const localSource = entry.source;
|
|
1401
|
+
if (!(0, import_node_fs4.existsSync)(localSource.path)) {
|
|
1402
|
+
throw new Error(`Local marketplace path does not exist: ${localSource.path}`);
|
|
1403
|
+
}
|
|
1404
|
+
(0, import_node_fs4.rmSync)(entry.installLocation, { recursive: true, force: true });
|
|
1405
|
+
(0, import_node_fs4.cpSync)(localSource.path, entry.installLocation, { recursive: true });
|
|
1406
|
+
} else {
|
|
1407
|
+
const command = `git -C ${entry.installLocation} pull`;
|
|
1408
|
+
try {
|
|
1409
|
+
this.exec(command, { timeout: GIT_TIMEOUT_MS, stdio: "pipe" });
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1412
|
+
throw new Error(`Failed to update marketplace "${name}": ${message}`);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
entry.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1416
|
+
this.writeRegistry(registry);
|
|
1417
|
+
}
|
|
1418
|
+
/** List all registered marketplaces. */
|
|
1419
|
+
listMarketplaces() {
|
|
1420
|
+
const registry = this.readRegistry();
|
|
1421
|
+
return Object.entries(registry).map(([name, entry]) => ({
|
|
1422
|
+
name,
|
|
1423
|
+
source: entry.source,
|
|
1424
|
+
lastUpdated: entry.lastUpdated
|
|
1425
|
+
}));
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Read the marketplace manifest from a registered marketplace's clone.
|
|
1429
|
+
*/
|
|
1430
|
+
fetchManifest(marketplaceName) {
|
|
1431
|
+
const registry = this.readRegistry();
|
|
1432
|
+
const entry = registry[marketplaceName];
|
|
1433
|
+
if (!entry) {
|
|
1434
|
+
throw new Error(`Marketplace "${marketplaceName}" not found`);
|
|
1435
|
+
}
|
|
1436
|
+
const manifestPath = (0, import_node_path5.join)(entry.installLocation, ".claude-plugin", "marketplace.json");
|
|
1437
|
+
if (!(0, import_node_fs4.existsSync)(manifestPath)) {
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`Marketplace "${marketplaceName}" does not contain .claude-plugin/marketplace.json`
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
return this.readManifestFromPath(manifestPath);
|
|
1443
|
+
}
|
|
1444
|
+
/** Get the clone directory path for a registered marketplace. */
|
|
1445
|
+
getMarketplaceDir(name) {
|
|
1446
|
+
const registry = this.readRegistry();
|
|
1447
|
+
const entry = registry[name];
|
|
1448
|
+
if (!entry) {
|
|
1449
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1450
|
+
}
|
|
1451
|
+
return entry.installLocation;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Get the current git SHA (first 12 chars) for a marketplace clone.
|
|
1455
|
+
* Used as a version identifier when plugins lack explicit versions.
|
|
1456
|
+
*/
|
|
1457
|
+
getMarketplaceSha(name) {
|
|
1458
|
+
const dir = this.getMarketplaceDir(name);
|
|
1459
|
+
try {
|
|
1460
|
+
const result = this.exec(`git -C ${dir} rev-parse HEAD`, {
|
|
1461
|
+
timeout: GIT_TIMEOUT_MS,
|
|
1462
|
+
stdio: "pipe"
|
|
1463
|
+
});
|
|
1464
|
+
return result.toString().trim().slice(0, 12);
|
|
1465
|
+
} catch {
|
|
1466
|
+
return "unknown";
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
/** List all available plugins across all marketplaces. */
|
|
1470
|
+
listAvailablePlugins() {
|
|
1471
|
+
const results = [];
|
|
1472
|
+
const marketplaces = this.listMarketplaces();
|
|
1473
|
+
for (const { name } of marketplaces) {
|
|
1474
|
+
try {
|
|
1475
|
+
const manifest = this.fetchManifest(name);
|
|
1476
|
+
for (const plugin of manifest.plugins) {
|
|
1477
|
+
results.push({ ...plugin, marketplace: name });
|
|
1478
|
+
}
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
return results;
|
|
1483
|
+
}
|
|
1484
|
+
// --- Private helpers ---
|
|
1485
|
+
/** Resolve a marketplace source to a git clone URL. */
|
|
1486
|
+
resolveCloneUrl(source) {
|
|
1487
|
+
switch (source.type) {
|
|
1488
|
+
case "github":
|
|
1489
|
+
return `https://github.com/${source.repo}.git`;
|
|
1490
|
+
case "git":
|
|
1491
|
+
return source.url;
|
|
1492
|
+
case "local":
|
|
1493
|
+
throw new Error("Local source type does not use git cloning");
|
|
1494
|
+
case "url":
|
|
1495
|
+
throw new Error("URL marketplace source is not yet supported");
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Remove all installed plugins that belong to a given marketplace.
|
|
1500
|
+
* Reads installed_plugins.json, deletes cache directories for matching plugins,
|
|
1501
|
+
* and updates the registry.
|
|
1502
|
+
*/
|
|
1503
|
+
removeInstalledPluginsForMarketplace(marketplaceName) {
|
|
1504
|
+
const installedPath = (0, import_node_path5.join)(this.pluginsDir, "installed_plugins.json");
|
|
1505
|
+
if (!(0, import_node_fs4.existsSync)(installedPath)) return;
|
|
1506
|
+
let registry;
|
|
1507
|
+
try {
|
|
1508
|
+
const raw = (0, import_node_fs4.readFileSync)(installedPath, "utf-8");
|
|
1509
|
+
const data = JSON.parse(raw);
|
|
1510
|
+
if (typeof data !== "object" || data === null) return;
|
|
1511
|
+
registry = data;
|
|
1512
|
+
} catch {
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
let changed = false;
|
|
1516
|
+
for (const [pluginId, record] of Object.entries(registry)) {
|
|
1517
|
+
if (record.marketplace === marketplaceName) {
|
|
1518
|
+
if (record.installPath && (0, import_node_fs4.existsSync)(record.installPath)) {
|
|
1519
|
+
(0, import_node_fs4.rmSync)(record.installPath, { recursive: true, force: true });
|
|
1520
|
+
}
|
|
1521
|
+
delete registry[pluginId];
|
|
1522
|
+
changed = true;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (changed) {
|
|
1526
|
+
const dir = (0, import_node_path5.dirname)(installedPath);
|
|
1527
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
1528
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
1529
|
+
}
|
|
1530
|
+
(0, import_node_fs4.writeFileSync)(installedPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
/** Read and parse a marketplace.json from a file path. */
|
|
1534
|
+
readManifestFromPath(path) {
|
|
1535
|
+
const raw = (0, import_node_fs4.readFileSync)(path, "utf-8");
|
|
1536
|
+
const data = JSON.parse(raw);
|
|
1537
|
+
if (typeof data !== "object" || data === null) {
|
|
1538
|
+
throw new Error("Invalid marketplace manifest: not an object");
|
|
1539
|
+
}
|
|
1540
|
+
const obj = data;
|
|
1541
|
+
if (typeof obj.name !== "string") {
|
|
1542
|
+
throw new Error('Invalid marketplace manifest: missing "name" field');
|
|
1543
|
+
}
|
|
1544
|
+
return data;
|
|
1545
|
+
}
|
|
1546
|
+
/** Read the known_marketplaces.json registry. */
|
|
1547
|
+
readRegistry() {
|
|
1548
|
+
if (!(0, import_node_fs4.existsSync)(this.registryPath)) {
|
|
1549
|
+
return {};
|
|
1550
|
+
}
|
|
1551
|
+
try {
|
|
1552
|
+
const raw = (0, import_node_fs4.readFileSync)(this.registryPath, "utf-8");
|
|
1553
|
+
const data = JSON.parse(raw);
|
|
1554
|
+
if (typeof data === "object" && data !== null) {
|
|
1555
|
+
return data;
|
|
1556
|
+
}
|
|
1557
|
+
return {};
|
|
1558
|
+
} catch {
|
|
1559
|
+
return {};
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
/** Write the known_marketplaces.json registry. */
|
|
1563
|
+
writeRegistry(registry) {
|
|
1564
|
+
const dir = (0, import_node_path5.dirname)(this.registryPath);
|
|
1565
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
1566
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
1567
|
+
}
|
|
1568
|
+
(0, import_node_fs4.writeFileSync)(this.registryPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1569
|
+
}
|
|
1570
|
+
/** Default exec implementation using child_process. */
|
|
1571
|
+
defaultExec(command, options) {
|
|
1572
|
+
return (0, import_node_child_process2.execSync)(command, { timeout: options.timeout, stdio: "pipe" });
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
|
|
590
1576
|
// src/tools/agent-tool.ts
|
|
591
1577
|
var import_zod2 = require("zod");
|
|
592
1578
|
var import_agent_tools2 = require("@robota-sdk/agent-tools");
|
|
@@ -668,8 +1654,14 @@ var import_agent_tools7 = require("@robota-sdk/agent-tools");
|
|
|
668
1654
|
var import_agent_tools8 = require("@robota-sdk/agent-tools");
|
|
669
1655
|
// Annotate the CommonJS export names for ESM import in node:
|
|
670
1656
|
0 && (module.exports = {
|
|
1657
|
+
AgentExecutor,
|
|
1658
|
+
BundlePluginInstaller,
|
|
1659
|
+
BundlePluginLoader,
|
|
671
1660
|
DEFAULT_TOOL_DESCRIPTIONS,
|
|
672
1661
|
FileSessionLogger,
|
|
1662
|
+
MarketplaceClient,
|
|
1663
|
+
PluginSettingsStore,
|
|
1664
|
+
PromptExecutor,
|
|
673
1665
|
Session,
|
|
674
1666
|
SessionStore,
|
|
675
1667
|
SilentSessionLogger,
|