@robota-sdk/agent-sdk 3.0.0-beta.3 → 3.0.0-beta.30
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 +1228 -145
- package/dist/node/index.d.cts +564 -195
- package/dist/node/index.d.ts +564 -195
- package/dist/node/index.js +1229 -139
- package/package.json +5 -5
package/dist/node/index.js
CHANGED
|
@@ -1,13 +1,328 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
2
|
import { TRUST_TO_MODE } from "@robota-sdk/agent-core";
|
|
3
3
|
|
|
4
|
-
// src/
|
|
4
|
+
// src/hooks/prompt-executor.ts
|
|
5
|
+
function extractJson(raw) {
|
|
6
|
+
const codeBlockMatch = /```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/.exec(raw);
|
|
7
|
+
if (codeBlockMatch) {
|
|
8
|
+
return codeBlockMatch[1].trim();
|
|
9
|
+
}
|
|
10
|
+
return raw.trim();
|
|
11
|
+
}
|
|
12
|
+
var PromptExecutor = class {
|
|
13
|
+
type = "prompt";
|
|
14
|
+
providerFactory;
|
|
15
|
+
defaultModel;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.providerFactory = options.providerFactory;
|
|
18
|
+
this.defaultModel = options.defaultModel;
|
|
19
|
+
}
|
|
20
|
+
async execute(definition, input) {
|
|
21
|
+
const promptDef = definition;
|
|
22
|
+
const model = promptDef.model ?? this.defaultModel;
|
|
23
|
+
try {
|
|
24
|
+
const provider = this.providerFactory(model);
|
|
25
|
+
const prompt = `${promptDef.prompt}
|
|
26
|
+
|
|
27
|
+
Context:
|
|
28
|
+
${JSON.stringify(input)}
|
|
29
|
+
|
|
30
|
+
Respond with JSON: { "ok": boolean, "reason"?: string }`;
|
|
31
|
+
const rawResponse = await provider.complete(prompt);
|
|
32
|
+
const jsonStr = extractJson(rawResponse);
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(jsonStr);
|
|
36
|
+
} catch {
|
|
37
|
+
return {
|
|
38
|
+
exitCode: 1,
|
|
39
|
+
stdout: "",
|
|
40
|
+
stderr: `Failed to parse AI response as JSON: ${rawResponse}`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (parsed.ok) {
|
|
44
|
+
return { exitCode: 0, stdout: JSON.stringify(parsed), stderr: "" };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
exitCode: 2,
|
|
48
|
+
stdout: "",
|
|
49
|
+
stderr: parsed.reason ?? "Blocked by prompt hook"
|
|
50
|
+
};
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
return { exitCode: 1, stdout: "", stderr: message };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/hooks/agent-executor.ts
|
|
59
|
+
var DEFAULT_MAX_TURNS = 50;
|
|
60
|
+
var DEFAULT_TIMEOUT_SECONDS = 60;
|
|
61
|
+
function extractJson2(raw) {
|
|
62
|
+
const codeBlockMatch = /```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/.exec(raw);
|
|
63
|
+
if (codeBlockMatch) {
|
|
64
|
+
return codeBlockMatch[1].trim();
|
|
65
|
+
}
|
|
66
|
+
return raw.trim();
|
|
67
|
+
}
|
|
68
|
+
var AgentExecutor = class {
|
|
69
|
+
type = "agent";
|
|
70
|
+
sessionFactory;
|
|
71
|
+
constructor(options) {
|
|
72
|
+
this.sessionFactory = options.sessionFactory;
|
|
73
|
+
}
|
|
74
|
+
async execute(definition, input) {
|
|
75
|
+
const agentDef = definition;
|
|
76
|
+
const maxTurns = agentDef.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
77
|
+
const timeout = agentDef.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
78
|
+
try {
|
|
79
|
+
const session = this.sessionFactory({ maxTurns, timeout });
|
|
80
|
+
const prompt = `Hook input:
|
|
81
|
+
${JSON.stringify(input)}
|
|
82
|
+
|
|
83
|
+
Respond with JSON: { "ok": boolean, "reason"?: string }`;
|
|
84
|
+
const rawResponse = await session.run(prompt);
|
|
85
|
+
const jsonStr = extractJson2(rawResponse);
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = JSON.parse(jsonStr);
|
|
89
|
+
} catch {
|
|
90
|
+
return {
|
|
91
|
+
exitCode: 1,
|
|
92
|
+
stdout: "",
|
|
93
|
+
stderr: `Failed to parse agent response as JSON: ${rawResponse}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (parsed.ok) {
|
|
97
|
+
return { exitCode: 0, stdout: JSON.stringify(parsed), stderr: "" };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
exitCode: 2,
|
|
101
|
+
stdout: "",
|
|
102
|
+
stderr: parsed.reason ?? "Blocked by agent hook"
|
|
103
|
+
};
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
return { exitCode: 1, stdout: "", stderr: message };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/assembly/create-session.ts
|
|
5
112
|
import { Session } from "@robota-sdk/agent-sessions";
|
|
6
113
|
|
|
114
|
+
// src/context/system-prompt-builder.ts
|
|
115
|
+
var TRUST_LEVEL_DESCRIPTIONS = {
|
|
116
|
+
safe: "safe (read-only / plan mode \u2014 only read-access tools are available)",
|
|
117
|
+
moderate: "moderate (default mode \u2014 write and bash tools require approval)",
|
|
118
|
+
full: "full (acceptEdits mode \u2014 file writes are auto-approved; bash requires approval)"
|
|
119
|
+
};
|
|
120
|
+
function buildProjectSection(info) {
|
|
121
|
+
const lines = ["## Current Project"];
|
|
122
|
+
if (info.name !== void 0) {
|
|
123
|
+
lines.push(`- **Name:** ${info.name}`);
|
|
124
|
+
}
|
|
125
|
+
if (info.type !== "unknown") {
|
|
126
|
+
lines.push(`- **Type:** ${info.type}`);
|
|
127
|
+
}
|
|
128
|
+
if (info.language !== "unknown") {
|
|
129
|
+
lines.push(`- **Language:** ${info.language}`);
|
|
130
|
+
}
|
|
131
|
+
if (info.packageManager !== void 0) {
|
|
132
|
+
lines.push(`- **Package manager:** ${info.packageManager}`);
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
function buildToolsSection(descriptions) {
|
|
137
|
+
if (descriptions.length === 0) {
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
const lines = ["## Available Tools", ...descriptions.map((d) => `- ${d}`)];
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
function buildSkillsSection(skills) {
|
|
144
|
+
const invocable = skills.filter((s) => s.disableModelInvocation !== true);
|
|
145
|
+
if (invocable.length === 0) {
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
const lines = [
|
|
149
|
+
"## Skills",
|
|
150
|
+
"The following skills are available:",
|
|
151
|
+
"",
|
|
152
|
+
...invocable.map((s) => `- ${s.name}: ${s.description}`)
|
|
153
|
+
];
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
function buildSystemPrompt(params) {
|
|
157
|
+
const { agentsMd, claudeMd, toolDescriptions, trustLevel, projectInfo, cwd, language } = params;
|
|
158
|
+
const sections = [];
|
|
159
|
+
const roleLines = [
|
|
160
|
+
"## Role",
|
|
161
|
+
"You are an AI coding assistant with access to tools that let you read and modify code.",
|
|
162
|
+
"You help developers understand, write, and improve their codebase.",
|
|
163
|
+
"Always be precise, follow existing code conventions, and prefer minimal changes."
|
|
164
|
+
];
|
|
165
|
+
if (language) {
|
|
166
|
+
roleLines.push(
|
|
167
|
+
`Always respond in ${language}. Use ${language} for all explanations and communications.`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
sections.push(roleLines.join("\n"));
|
|
171
|
+
if (cwd) {
|
|
172
|
+
sections.push(`## Working Directory
|
|
173
|
+
\`${cwd}\``);
|
|
174
|
+
}
|
|
175
|
+
sections.push(buildProjectSection(projectInfo));
|
|
176
|
+
sections.push(
|
|
177
|
+
[
|
|
178
|
+
"## Permission Mode",
|
|
179
|
+
`Your current trust level is **${TRUST_LEVEL_DESCRIPTIONS[trustLevel]}**.`
|
|
180
|
+
].join("\n")
|
|
181
|
+
);
|
|
182
|
+
if (agentsMd.trim().length > 0) {
|
|
183
|
+
sections.push(["## Agent Instructions", agentsMd].join("\n"));
|
|
184
|
+
}
|
|
185
|
+
if (claudeMd.trim().length > 0) {
|
|
186
|
+
sections.push(["## Project Notes", claudeMd].join("\n"));
|
|
187
|
+
}
|
|
188
|
+
sections.push(
|
|
189
|
+
[
|
|
190
|
+
"## Web Search",
|
|
191
|
+
"You have access to web search. When the user asks to search, look up, or find current/latest information,",
|
|
192
|
+
"you MUST use the web_search tool. Do NOT answer from training data when the user explicitly asks to search.",
|
|
193
|
+
"Always prefer web search for: news, latest versions, current events, live documentation."
|
|
194
|
+
].join("\n")
|
|
195
|
+
);
|
|
196
|
+
const toolsSection = buildToolsSection(toolDescriptions);
|
|
197
|
+
if (toolsSection.length > 0) {
|
|
198
|
+
sections.push(toolsSection);
|
|
199
|
+
}
|
|
200
|
+
if (params.skills !== void 0 && params.skills.length > 0) {
|
|
201
|
+
const skillsSection = buildSkillsSection(params.skills);
|
|
202
|
+
if (skillsSection.length > 0) {
|
|
203
|
+
sections.push(skillsSection);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return sections.join("\n\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/assembly/create-tools.ts
|
|
210
|
+
import {
|
|
211
|
+
bashTool,
|
|
212
|
+
readTool,
|
|
213
|
+
writeTool,
|
|
214
|
+
editTool,
|
|
215
|
+
globTool,
|
|
216
|
+
grepTool,
|
|
217
|
+
webFetchTool,
|
|
218
|
+
webSearchTool
|
|
219
|
+
} from "@robota-sdk/agent-tools";
|
|
220
|
+
var DEFAULT_TOOL_DESCRIPTIONS = [
|
|
221
|
+
"Bash \u2014 execute shell commands",
|
|
222
|
+
"Read \u2014 read file contents with line numbers",
|
|
223
|
+
"Write \u2014 write content to a file",
|
|
224
|
+
"Edit \u2014 replace a string in a file",
|
|
225
|
+
"Glob \u2014 find files matching a pattern",
|
|
226
|
+
"Grep \u2014 search file contents with regex",
|
|
227
|
+
"WebSearch \u2014 search the internet (Anthropic built-in)"
|
|
228
|
+
];
|
|
229
|
+
function createDefaultTools() {
|
|
230
|
+
return [
|
|
231
|
+
bashTool,
|
|
232
|
+
readTool,
|
|
233
|
+
writeTool,
|
|
234
|
+
editTool,
|
|
235
|
+
globTool,
|
|
236
|
+
grepTool,
|
|
237
|
+
webFetchTool,
|
|
238
|
+
webSearchTool
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/assembly/create-provider.ts
|
|
243
|
+
import { AnthropicProvider } from "@robota-sdk/agent-provider-anthropic";
|
|
244
|
+
function createProvider(config) {
|
|
245
|
+
const apiKey = config.provider.apiKey ?? process.env["ANTHROPIC_API_KEY"];
|
|
246
|
+
if (!apiKey) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"ANTHROPIC_API_KEY is not set. Set the environment variable or configure provider.apiKey in ~/.robota/settings.json"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return new AnthropicProvider({ apiKey });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/assembly/create-session.ts
|
|
255
|
+
function createSession(options) {
|
|
256
|
+
const provider = options.provider ?? createProvider(options.config);
|
|
257
|
+
const defaultTools = createDefaultTools();
|
|
258
|
+
const tools = [...defaultTools, ...options.additionalTools ?? []];
|
|
259
|
+
const buildPrompt = options.systemPromptBuilder ?? buildSystemPrompt;
|
|
260
|
+
const systemMessage = buildPrompt({
|
|
261
|
+
agentsMd: options.context.agentsMd,
|
|
262
|
+
claudeMd: options.context.claudeMd,
|
|
263
|
+
toolDescriptions: options.toolDescriptions ?? DEFAULT_TOOL_DESCRIPTIONS,
|
|
264
|
+
trustLevel: options.config.defaultTrustLevel,
|
|
265
|
+
projectInfo: options.projectInfo ?? { type: "unknown", language: "unknown" },
|
|
266
|
+
cwd: process.cwd(),
|
|
267
|
+
language: options.config.language
|
|
268
|
+
});
|
|
269
|
+
const defaultAllow = [
|
|
270
|
+
"Read(.agents/**)",
|
|
271
|
+
"Read(.claude/**)",
|
|
272
|
+
"Read(.robota/**)",
|
|
273
|
+
"Glob(.agents/**)",
|
|
274
|
+
"Glob(.claude/**)",
|
|
275
|
+
"Glob(.robota/**)"
|
|
276
|
+
];
|
|
277
|
+
const mergedPermissions = {
|
|
278
|
+
allow: [...defaultAllow, ...options.config.permissions.allow ?? []],
|
|
279
|
+
deny: options.config.permissions.deny ?? []
|
|
280
|
+
};
|
|
281
|
+
const hookTypeExecutors = [];
|
|
282
|
+
if (options.providerFactory) {
|
|
283
|
+
hookTypeExecutors.push(
|
|
284
|
+
new PromptExecutor({
|
|
285
|
+
providerFactory: options.providerFactory,
|
|
286
|
+
defaultModel: options.config.provider.model
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (options.sessionFactory) {
|
|
291
|
+
hookTypeExecutors.push(
|
|
292
|
+
new AgentExecutor({
|
|
293
|
+
sessionFactory: options.sessionFactory
|
|
294
|
+
})
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (options.additionalHookExecutors) {
|
|
298
|
+
hookTypeExecutors.push(...options.additionalHookExecutors);
|
|
299
|
+
}
|
|
300
|
+
return new Session({
|
|
301
|
+
tools,
|
|
302
|
+
provider,
|
|
303
|
+
systemMessage,
|
|
304
|
+
terminal: options.terminal,
|
|
305
|
+
permissions: mergedPermissions,
|
|
306
|
+
hooks: options.config.hooks,
|
|
307
|
+
permissionMode: options.permissionMode,
|
|
308
|
+
defaultTrustLevel: options.config.defaultTrustLevel,
|
|
309
|
+
model: options.config.provider.model,
|
|
310
|
+
maxTurns: options.maxTurns,
|
|
311
|
+
sessionStore: options.sessionStore,
|
|
312
|
+
permissionHandler: options.permissionHandler,
|
|
313
|
+
onTextDelta: options.onTextDelta,
|
|
314
|
+
onToolExecution: options.onToolExecution,
|
|
315
|
+
promptForApproval: options.promptForApproval,
|
|
316
|
+
onCompact: options.onCompact,
|
|
317
|
+
compactInstructions: options.compactInstructions ?? options.context.compactInstructions,
|
|
318
|
+
sessionLogger: options.sessionLogger,
|
|
319
|
+
hookTypeExecutors: hookTypeExecutors.length > 0 ? hookTypeExecutors : void 0
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
7
323
|
// src/index.ts
|
|
324
|
+
import { Session as Session2 } from "@robota-sdk/agent-sessions";
|
|
8
325
|
import { FileSessionLogger, SilentSessionLogger } from "@robota-sdk/agent-sessions";
|
|
9
|
-
|
|
10
|
-
// src/session-store.ts
|
|
11
326
|
import { SessionStore } from "@robota-sdk/agent-sessions";
|
|
12
327
|
|
|
13
328
|
// src/config/config-loader.ts
|
|
@@ -28,10 +343,34 @@ var PermissionsSchema = z.object({
|
|
|
28
343
|
deny: z.array(z.string()).optional()
|
|
29
344
|
});
|
|
30
345
|
var EnvSchema = z.record(z.string()).optional();
|
|
31
|
-
var
|
|
346
|
+
var CommandHookDefinitionSchema = z.object({
|
|
32
347
|
type: z.literal("command"),
|
|
33
|
-
command: z.string()
|
|
348
|
+
command: z.string(),
|
|
349
|
+
timeout: z.number().optional()
|
|
350
|
+
});
|
|
351
|
+
var HttpHookDefinitionSchema = z.object({
|
|
352
|
+
type: z.literal("http"),
|
|
353
|
+
url: z.string(),
|
|
354
|
+
headers: z.record(z.string()).optional(),
|
|
355
|
+
timeout: z.number().optional()
|
|
34
356
|
});
|
|
357
|
+
var PromptHookDefinitionSchema = z.object({
|
|
358
|
+
type: z.literal("prompt"),
|
|
359
|
+
prompt: z.string(),
|
|
360
|
+
model: z.string().optional()
|
|
361
|
+
});
|
|
362
|
+
var AgentHookDefinitionSchema = z.object({
|
|
363
|
+
type: z.literal("agent"),
|
|
364
|
+
agent: z.string(),
|
|
365
|
+
maxTurns: z.number().optional(),
|
|
366
|
+
timeout: z.number().optional()
|
|
367
|
+
});
|
|
368
|
+
var HookDefinitionSchema = z.discriminatedUnion("type", [
|
|
369
|
+
CommandHookDefinitionSchema,
|
|
370
|
+
HttpHookDefinitionSchema,
|
|
371
|
+
PromptHookDefinitionSchema,
|
|
372
|
+
AgentHookDefinitionSchema
|
|
373
|
+
]);
|
|
35
374
|
var HookGroupSchema = z.object({
|
|
36
375
|
matcher: z.string(),
|
|
37
376
|
hooks: z.array(HookDefinitionSchema)
|
|
@@ -40,15 +379,36 @@ var HooksSchema = z.object({
|
|
|
40
379
|
PreToolUse: z.array(HookGroupSchema).optional(),
|
|
41
380
|
PostToolUse: z.array(HookGroupSchema).optional(),
|
|
42
381
|
SessionStart: z.array(HookGroupSchema).optional(),
|
|
43
|
-
Stop: z.array(HookGroupSchema).optional()
|
|
382
|
+
Stop: z.array(HookGroupSchema).optional(),
|
|
383
|
+
PreCompact: z.array(HookGroupSchema).optional(),
|
|
384
|
+
PostCompact: z.array(HookGroupSchema).optional(),
|
|
385
|
+
UserPromptSubmit: z.array(HookGroupSchema).optional(),
|
|
386
|
+
Notification: z.array(HookGroupSchema).optional()
|
|
44
387
|
}).optional();
|
|
388
|
+
var EnabledPluginsSchema = z.record(z.boolean()).optional();
|
|
389
|
+
var MarketplaceSourceSchema = z.object({
|
|
390
|
+
source: z.object({
|
|
391
|
+
type: z.enum(["github", "git", "local", "url"]),
|
|
392
|
+
repo: z.string().optional(),
|
|
393
|
+
url: z.string().optional(),
|
|
394
|
+
path: z.string().optional(),
|
|
395
|
+
ref: z.string().optional()
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
var ExtraKnownMarketplacesSchema = z.record(MarketplaceSourceSchema).optional();
|
|
45
399
|
var SettingsSchema = z.object({
|
|
46
400
|
/** Trust level used when no --permission-mode flag is given */
|
|
47
401
|
defaultTrustLevel: z.enum(["safe", "moderate", "full"]).optional(),
|
|
402
|
+
/** Response language (e.g., "ko", "en", "ja"). Injected into system prompt. */
|
|
403
|
+
language: z.string().optional(),
|
|
48
404
|
provider: ProviderSchema.optional(),
|
|
49
405
|
permissions: PermissionsSchema.optional(),
|
|
50
406
|
env: EnvSchema,
|
|
51
|
-
hooks: HooksSchema
|
|
407
|
+
hooks: HooksSchema,
|
|
408
|
+
/** Plugin enablement map: plugin name -> enabled/disabled */
|
|
409
|
+
enabledPlugins: EnabledPluginsSchema,
|
|
410
|
+
/** Extra marketplace URLs for BundlePlugin discovery */
|
|
411
|
+
extraKnownMarketplaces: ExtraKnownMarketplacesSchema
|
|
52
412
|
});
|
|
53
413
|
|
|
54
414
|
// src/config/config-loader.ts
|
|
@@ -72,8 +432,15 @@ function readJsonFile(filePath) {
|
|
|
72
432
|
if (!existsSync(filePath)) {
|
|
73
433
|
return void 0;
|
|
74
434
|
}
|
|
75
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
76
|
-
|
|
435
|
+
const raw = readFileSync(filePath, "utf-8").trim();
|
|
436
|
+
if (raw.length === 0) {
|
|
437
|
+
return void 0;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
return JSON.parse(raw);
|
|
441
|
+
} catch {
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
77
444
|
}
|
|
78
445
|
function resolveEnvRef(value) {
|
|
79
446
|
const ENV_PREFIX = "$ENV:";
|
|
@@ -108,13 +475,16 @@ function mergeSettings(layers) {
|
|
|
108
475
|
env: {
|
|
109
476
|
...merged.env ?? {},
|
|
110
477
|
...layer.env ?? {}
|
|
111
|
-
}
|
|
478
|
+
},
|
|
479
|
+
enabledPlugins: merged.enabledPlugins !== void 0 || layer.enabledPlugins !== void 0 ? { ...merged.enabledPlugins ?? {}, ...layer.enabledPlugins ?? {} } : void 0,
|
|
480
|
+
extraKnownMarketplaces: layer.extraKnownMarketplaces ?? merged.extraKnownMarketplaces
|
|
112
481
|
};
|
|
113
482
|
}, {});
|
|
114
483
|
}
|
|
115
484
|
function toResolvedConfig(merged) {
|
|
116
485
|
return {
|
|
117
486
|
defaultTrustLevel: merged.defaultTrustLevel ?? DEFAULTS.defaultTrustLevel,
|
|
487
|
+
language: merged.language,
|
|
118
488
|
provider: {
|
|
119
489
|
name: merged.provider?.name ?? DEFAULTS.provider.name,
|
|
120
490
|
model: merged.provider?.model ?? DEFAULTS.provider.model,
|
|
@@ -125,25 +495,39 @@ function toResolvedConfig(merged) {
|
|
|
125
495
|
deny: merged.permissions?.deny ?? DEFAULTS.permissions.deny
|
|
126
496
|
},
|
|
127
497
|
env: merged.env ?? DEFAULTS.env,
|
|
128
|
-
hooks: merged.hooks ?? void 0
|
|
498
|
+
hooks: merged.hooks ?? void 0,
|
|
499
|
+
enabledPlugins: merged.enabledPlugins ?? void 0,
|
|
500
|
+
extraKnownMarketplaces: merged.extraKnownMarketplaces ?? void 0
|
|
129
501
|
};
|
|
130
502
|
}
|
|
503
|
+
function getSettingsPaths(cwd) {
|
|
504
|
+
const home = getHomeDir();
|
|
505
|
+
return [
|
|
506
|
+
join(home, ".robota", "settings.json"),
|
|
507
|
+
// 1. user (lowest)
|
|
508
|
+
join(cwd, ".robota", "settings.json"),
|
|
509
|
+
// 2. project
|
|
510
|
+
join(cwd, ".robota", "settings.local.json"),
|
|
511
|
+
// 3. project-local
|
|
512
|
+
join(cwd, ".claude", "settings.json"),
|
|
513
|
+
// 4. project, Claude Code compat
|
|
514
|
+
join(cwd, ".claude", "settings.local.json")
|
|
515
|
+
// 5. project-local (highest)
|
|
516
|
+
];
|
|
517
|
+
}
|
|
131
518
|
async function loadConfig(cwd) {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const parsedLayers =
|
|
519
|
+
const allPaths = getSettingsPaths(cwd);
|
|
520
|
+
const rawEntries = [];
|
|
521
|
+
for (const filePath of allPaths) {
|
|
522
|
+
const raw = readJsonFile(filePath);
|
|
523
|
+
if (raw !== void 0) {
|
|
524
|
+
rawEntries.push({ raw, path: filePath });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const parsedLayers = rawEntries.map(({ raw, path }) => {
|
|
141
528
|
const result = SettingsSchema.safeParse(raw);
|
|
142
529
|
if (!result.success) {
|
|
143
|
-
|
|
144
|
-
(_, i) => rawLayers[i] !== void 0
|
|
145
|
-
);
|
|
146
|
-
throw new Error(`Invalid settings in ${paths[index] ?? "unknown"}: ${result.error.message}`);
|
|
530
|
+
throw new Error(`Invalid settings in ${path}: ${result.error.message}`);
|
|
147
531
|
}
|
|
148
532
|
return resolveEnvRefs(result.data);
|
|
149
533
|
});
|
|
@@ -273,77 +657,6 @@ async function detectProject(cwd) {
|
|
|
273
657
|
};
|
|
274
658
|
}
|
|
275
659
|
|
|
276
|
-
// src/context/system-prompt-builder.ts
|
|
277
|
-
var TRUST_LEVEL_DESCRIPTIONS = {
|
|
278
|
-
safe: "safe (read-only / plan mode \u2014 only read-access tools are available)",
|
|
279
|
-
moderate: "moderate (default mode \u2014 write and bash tools require approval)",
|
|
280
|
-
full: "full (acceptEdits mode \u2014 file writes are auto-approved; bash requires approval)"
|
|
281
|
-
};
|
|
282
|
-
function buildProjectSection(info) {
|
|
283
|
-
const lines = ["## Current Project"];
|
|
284
|
-
if (info.name !== void 0) {
|
|
285
|
-
lines.push(`- **Name:** ${info.name}`);
|
|
286
|
-
}
|
|
287
|
-
if (info.type !== "unknown") {
|
|
288
|
-
lines.push(`- **Type:** ${info.type}`);
|
|
289
|
-
}
|
|
290
|
-
if (info.language !== "unknown") {
|
|
291
|
-
lines.push(`- **Language:** ${info.language}`);
|
|
292
|
-
}
|
|
293
|
-
if (info.packageManager !== void 0) {
|
|
294
|
-
lines.push(`- **Package manager:** ${info.packageManager}`);
|
|
295
|
-
}
|
|
296
|
-
return lines.join("\n");
|
|
297
|
-
}
|
|
298
|
-
function buildToolsSection(descriptions) {
|
|
299
|
-
if (descriptions.length === 0) {
|
|
300
|
-
return "";
|
|
301
|
-
}
|
|
302
|
-
const lines = ["## Available Tools", ...descriptions.map((d) => `- ${d}`)];
|
|
303
|
-
return lines.join("\n");
|
|
304
|
-
}
|
|
305
|
-
function buildSystemPrompt(params) {
|
|
306
|
-
const { agentsMd, claudeMd, toolDescriptions, trustLevel, projectInfo } = params;
|
|
307
|
-
const sections = [];
|
|
308
|
-
sections.push(
|
|
309
|
-
[
|
|
310
|
-
"## Role",
|
|
311
|
-
"You are an AI coding assistant with access to tools that let you read and modify code.",
|
|
312
|
-
"You help developers understand, write, and improve their codebase.",
|
|
313
|
-
"Always be precise, follow existing code conventions, and prefer minimal changes."
|
|
314
|
-
].join("\n")
|
|
315
|
-
);
|
|
316
|
-
sections.push(buildProjectSection(projectInfo));
|
|
317
|
-
sections.push(
|
|
318
|
-
[
|
|
319
|
-
"## Permission Mode",
|
|
320
|
-
`Your current trust level is **${TRUST_LEVEL_DESCRIPTIONS[trustLevel]}**.`
|
|
321
|
-
].join("\n")
|
|
322
|
-
);
|
|
323
|
-
if (agentsMd.trim().length > 0) {
|
|
324
|
-
sections.push(["## Agent Instructions", agentsMd].join("\n"));
|
|
325
|
-
}
|
|
326
|
-
if (claudeMd.trim().length > 0) {
|
|
327
|
-
sections.push(["## Project Notes", claudeMd].join("\n"));
|
|
328
|
-
}
|
|
329
|
-
sections.push(
|
|
330
|
-
[
|
|
331
|
-
"## Web Search",
|
|
332
|
-
"You have access to web search. When the user asks to search, look up, or find current/latest information,",
|
|
333
|
-
"you MUST use the web_search tool. Do NOT answer from training data when the user explicitly asks to search.",
|
|
334
|
-
"Always prefer web search for: news, latest versions, current events, live documentation."
|
|
335
|
-
].join("\n")
|
|
336
|
-
);
|
|
337
|
-
const toolsSection = buildToolsSection(toolDescriptions);
|
|
338
|
-
if (toolsSection.length > 0) {
|
|
339
|
-
sections.push(toolsSection);
|
|
340
|
-
}
|
|
341
|
-
return sections.join("\n\n");
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// src/query.ts
|
|
345
|
-
import { Session as Session2 } from "@robota-sdk/agent-sessions";
|
|
346
|
-
|
|
347
660
|
// src/permissions/permission-prompt.ts
|
|
348
661
|
import chalk from "chalk";
|
|
349
662
|
var PERMISSION_OPTIONS = ["Allow", "Deny"];
|
|
@@ -387,7 +700,7 @@ async function query(prompt, options) {
|
|
|
387
700
|
}, update: () => {
|
|
388
701
|
} })
|
|
389
702
|
};
|
|
390
|
-
const session =
|
|
703
|
+
const session = createSession({
|
|
391
704
|
config,
|
|
392
705
|
context,
|
|
393
706
|
terminal: noopTerminal,
|
|
@@ -399,16 +712,13 @@ async function query(prompt, options) {
|
|
|
399
712
|
onTextDelta: options?.onTextDelta,
|
|
400
713
|
onCompact: options?.onCompact,
|
|
401
714
|
compactInstructions: context.compactInstructions,
|
|
402
|
-
systemPromptBuilder: buildSystemPrompt,
|
|
403
715
|
promptForApproval
|
|
404
716
|
});
|
|
405
717
|
return session.run(prompt);
|
|
406
718
|
}
|
|
407
719
|
|
|
408
|
-
// src/
|
|
720
|
+
// src/index.ts
|
|
409
721
|
import { evaluatePermission } from "@robota-sdk/agent-core";
|
|
410
|
-
|
|
411
|
-
// src/hooks/hook-runner.ts
|
|
412
722
|
import { runHooks } from "@robota-sdk/agent-core";
|
|
413
723
|
|
|
414
724
|
// src/paths.ts
|
|
@@ -431,28 +741,790 @@ function userPaths() {
|
|
|
431
741
|
};
|
|
432
742
|
}
|
|
433
743
|
|
|
434
|
-
// src/
|
|
435
|
-
import {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
744
|
+
// src/plugins/plugin-settings-store.ts
|
|
745
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync, mkdirSync } from "fs";
|
|
746
|
+
import { dirname as dirname2 } from "path";
|
|
747
|
+
var PluginSettingsStore = class {
|
|
748
|
+
settingsPath;
|
|
749
|
+
constructor(settingsPath) {
|
|
750
|
+
this.settingsPath = settingsPath;
|
|
751
|
+
}
|
|
752
|
+
/** Read the full settings file from disk. */
|
|
753
|
+
readAll() {
|
|
754
|
+
if (!existsSync4(this.settingsPath)) {
|
|
755
|
+
return {};
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
const raw = readFileSync4(this.settingsPath, "utf-8");
|
|
759
|
+
const data = JSON.parse(raw);
|
|
760
|
+
if (typeof data === "object" && data !== null) {
|
|
761
|
+
return data;
|
|
762
|
+
}
|
|
763
|
+
return {};
|
|
764
|
+
} catch {
|
|
765
|
+
return {};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/** Write the full settings file to disk. */
|
|
769
|
+
writeAll(settings) {
|
|
770
|
+
const dir = dirname2(this.settingsPath);
|
|
771
|
+
if (!existsSync4(dir)) {
|
|
772
|
+
mkdirSync(dir, { recursive: true });
|
|
773
|
+
}
|
|
774
|
+
writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
775
|
+
}
|
|
776
|
+
// --- enabledPlugins ---
|
|
777
|
+
/** Get the enabledPlugins map. */
|
|
778
|
+
getEnabledPlugins() {
|
|
779
|
+
const settings = this.readAll();
|
|
780
|
+
const ep = settings.enabledPlugins;
|
|
781
|
+
if (typeof ep === "object" && ep !== null) {
|
|
782
|
+
return ep;
|
|
783
|
+
}
|
|
784
|
+
return {};
|
|
785
|
+
}
|
|
786
|
+
/** Set a single plugin's enabled state. */
|
|
787
|
+
setPluginEnabled(pluginId, enabled) {
|
|
788
|
+
const settings = this.readAll();
|
|
789
|
+
const ep = this.getEnabledPluginsFrom(settings);
|
|
790
|
+
ep[pluginId] = enabled;
|
|
791
|
+
settings.enabledPlugins = ep;
|
|
792
|
+
this.writeAll(settings);
|
|
793
|
+
}
|
|
794
|
+
/** Remove a plugin from enabledPlugins. */
|
|
795
|
+
removePluginEntry(pluginId) {
|
|
796
|
+
const settings = this.readAll();
|
|
797
|
+
const ep = this.getEnabledPluginsFrom(settings);
|
|
798
|
+
delete ep[pluginId];
|
|
799
|
+
settings.enabledPlugins = ep;
|
|
800
|
+
this.writeAll(settings);
|
|
801
|
+
}
|
|
802
|
+
// --- extraKnownMarketplaces ---
|
|
803
|
+
/** Get all persisted marketplace sources. */
|
|
804
|
+
getMarketplaceSources() {
|
|
805
|
+
const settings = this.readAll();
|
|
806
|
+
const extra = settings.extraKnownMarketplaces;
|
|
807
|
+
if (typeof extra === "object" && extra !== null) {
|
|
808
|
+
return extra;
|
|
809
|
+
}
|
|
810
|
+
return {};
|
|
811
|
+
}
|
|
812
|
+
/** Add or update a marketplace source. */
|
|
813
|
+
setMarketplaceSource(name, source) {
|
|
814
|
+
const settings = this.readAll();
|
|
815
|
+
const extra = this.getMarketplaceSourcesFrom(settings);
|
|
816
|
+
extra[name] = { source };
|
|
817
|
+
settings.extraKnownMarketplaces = extra;
|
|
818
|
+
this.writeAll(settings);
|
|
819
|
+
}
|
|
820
|
+
/** Remove a marketplace source. */
|
|
821
|
+
removeMarketplaceSource(name) {
|
|
822
|
+
const settings = this.readAll();
|
|
823
|
+
const extra = this.getMarketplaceSourcesFrom(settings);
|
|
824
|
+
delete extra[name];
|
|
825
|
+
settings.extraKnownMarketplaces = extra;
|
|
826
|
+
this.writeAll(settings);
|
|
827
|
+
}
|
|
828
|
+
// --- helpers ---
|
|
829
|
+
getEnabledPluginsFrom(settings) {
|
|
830
|
+
const ep = settings.enabledPlugins;
|
|
831
|
+
if (typeof ep === "object" && ep !== null) {
|
|
832
|
+
return ep;
|
|
833
|
+
}
|
|
834
|
+
return {};
|
|
835
|
+
}
|
|
836
|
+
getMarketplaceSourcesFrom(settings) {
|
|
837
|
+
const extra = settings.extraKnownMarketplaces;
|
|
838
|
+
if (typeof extra === "object" && extra !== null) {
|
|
839
|
+
return extra;
|
|
840
|
+
}
|
|
841
|
+
return {};
|
|
842
|
+
}
|
|
843
|
+
};
|
|
442
844
|
|
|
443
|
-
// src/
|
|
444
|
-
import {
|
|
845
|
+
// src/plugins/bundle-plugin-loader.ts
|
|
846
|
+
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
847
|
+
import { join as join5 } from "path";
|
|
848
|
+
function parseSkillFrontmatter(raw) {
|
|
849
|
+
const trimmed = raw.trimStart();
|
|
850
|
+
if (!trimmed.startsWith("---")) {
|
|
851
|
+
return { metadata: {}, content: raw };
|
|
852
|
+
}
|
|
853
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
854
|
+
if (endIndex === -1) {
|
|
855
|
+
return { metadata: {}, content: raw };
|
|
856
|
+
}
|
|
857
|
+
const frontmatterBlock = trimmed.slice(3, endIndex).trim();
|
|
858
|
+
const content = trimmed.slice(endIndex + 3).trimStart();
|
|
859
|
+
const metadata = {};
|
|
860
|
+
for (const line of frontmatterBlock.split("\n")) {
|
|
861
|
+
const colonIndex = line.indexOf(":");
|
|
862
|
+
if (colonIndex === -1) continue;
|
|
863
|
+
const key = line.slice(0, colonIndex).trim();
|
|
864
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
865
|
+
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
|
|
866
|
+
const inner = value.slice(1, -1);
|
|
867
|
+
value = inner.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
868
|
+
}
|
|
869
|
+
if (key) {
|
|
870
|
+
metadata[key] = value;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return { metadata, content };
|
|
874
|
+
}
|
|
875
|
+
function validateManifest(data) {
|
|
876
|
+
if (typeof data !== "object" || data === null) return null;
|
|
877
|
+
const obj = data;
|
|
878
|
+
if (typeof obj.name !== "string") return null;
|
|
879
|
+
if (typeof obj.version !== "string") return null;
|
|
880
|
+
if (typeof obj.description !== "string") return null;
|
|
881
|
+
const features = typeof obj.features === "object" && obj.features !== null ? obj.features : {};
|
|
882
|
+
return {
|
|
883
|
+
name: obj.name,
|
|
884
|
+
version: obj.version,
|
|
885
|
+
description: obj.description,
|
|
886
|
+
features: {
|
|
887
|
+
commands: features.commands === true ? true : void 0,
|
|
888
|
+
agents: features.agents === true ? true : void 0,
|
|
889
|
+
skills: features.skills === true ? true : void 0,
|
|
890
|
+
hooks: features.hooks === true ? true : void 0,
|
|
891
|
+
mcp: features.mcp === true ? true : void 0
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function getSortedSubdirs(dirPath) {
|
|
896
|
+
if (!existsSync5(dirPath)) return [];
|
|
897
|
+
try {
|
|
898
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
899
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
900
|
+
} catch {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
var BundlePluginLoader = class {
|
|
905
|
+
pluginsDir;
|
|
906
|
+
enabledPlugins;
|
|
907
|
+
constructor(pluginsDir, enabledPlugins) {
|
|
908
|
+
this.pluginsDir = pluginsDir;
|
|
909
|
+
this.enabledPlugins = enabledPlugins ?? {};
|
|
910
|
+
}
|
|
911
|
+
/** Load all discovered and enabled bundle plugins (sync). */
|
|
912
|
+
loadPluginsSync() {
|
|
913
|
+
return this.discoverAndLoad();
|
|
914
|
+
}
|
|
915
|
+
/** Load all discovered and enabled bundle plugins (async wrapper). */
|
|
916
|
+
async loadAll() {
|
|
917
|
+
return this.discoverAndLoad();
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Discover and load plugins from the cache directory.
|
|
921
|
+
*
|
|
922
|
+
* Directory structure: `<pluginsDir>/cache/<marketplace>/<plugin>/<version>/`
|
|
923
|
+
* For each marketplace/plugin pair, the latest version (lexicographically last) is loaded.
|
|
924
|
+
*/
|
|
925
|
+
discoverAndLoad() {
|
|
926
|
+
const cacheDir = join5(this.pluginsDir, "cache");
|
|
927
|
+
if (!existsSync5(cacheDir)) {
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
const results = [];
|
|
931
|
+
const marketplaces = getSortedSubdirs(cacheDir);
|
|
932
|
+
for (const marketplace of marketplaces) {
|
|
933
|
+
const marketplaceDir = join5(cacheDir, marketplace);
|
|
934
|
+
const plugins = getSortedSubdirs(marketplaceDir);
|
|
935
|
+
for (const pluginName of plugins) {
|
|
936
|
+
const pluginDir = join5(marketplaceDir, pluginName);
|
|
937
|
+
const versions = getSortedSubdirs(pluginDir);
|
|
938
|
+
if (versions.length === 0) continue;
|
|
939
|
+
const latestVersion = versions[versions.length - 1];
|
|
940
|
+
const versionDir = join5(pluginDir, latestVersion);
|
|
941
|
+
const manifestPath = join5(versionDir, ".claude-plugin", "plugin.json");
|
|
942
|
+
if (!existsSync5(manifestPath)) continue;
|
|
943
|
+
const manifest = this.readManifest(manifestPath);
|
|
944
|
+
if (!manifest) continue;
|
|
945
|
+
const pluginId = `${manifest.name}@${marketplace}`;
|
|
946
|
+
if (this.isDisabled(pluginId, manifest.name)) continue;
|
|
947
|
+
const loaded = this.loadPlugin(versionDir, manifest);
|
|
948
|
+
results.push(loaded);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return results;
|
|
952
|
+
}
|
|
953
|
+
/** Read and validate a plugin.json manifest. Returns null on failure. */
|
|
954
|
+
readManifest(path) {
|
|
955
|
+
try {
|
|
956
|
+
const raw = readFileSync5(path, "utf-8");
|
|
957
|
+
const data = JSON.parse(raw);
|
|
958
|
+
return validateManifest(data);
|
|
959
|
+
} catch {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Check if a plugin is explicitly disabled.
|
|
965
|
+
* Checks both `name@marketplace` and `name` keys.
|
|
966
|
+
* Plugins not listed in enabledPlugins are enabled by default.
|
|
967
|
+
*/
|
|
968
|
+
isDisabled(pluginId, pluginName) {
|
|
969
|
+
if (pluginId in this.enabledPlugins) {
|
|
970
|
+
return this.enabledPlugins[pluginId] === false;
|
|
971
|
+
}
|
|
972
|
+
if (pluginName in this.enabledPlugins) {
|
|
973
|
+
return this.enabledPlugins[pluginName] === false;
|
|
974
|
+
}
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
/** Load a single plugin's skills, hooks, agents, and MCP config. */
|
|
978
|
+
loadPlugin(pluginDir, manifest) {
|
|
979
|
+
return {
|
|
980
|
+
manifest,
|
|
981
|
+
skills: this.loadSkills(pluginDir, manifest.name),
|
|
982
|
+
commands: this.loadCommands(pluginDir, manifest.name),
|
|
983
|
+
hooks: this.loadHooks(pluginDir),
|
|
984
|
+
mcpConfig: this.loadMcpConfig(pluginDir),
|
|
985
|
+
agents: this.loadAgents(pluginDir),
|
|
986
|
+
pluginDir
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
/** Load skills from the plugin's skills/ directory. */
|
|
990
|
+
loadSkills(pluginDir, pluginName) {
|
|
991
|
+
const skillsDir = join5(pluginDir, "skills");
|
|
992
|
+
if (!existsSync5(skillsDir)) return [];
|
|
993
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
994
|
+
const skills = [];
|
|
995
|
+
for (const entry of entries) {
|
|
996
|
+
if (!entry.isDirectory()) continue;
|
|
997
|
+
const skillFile = join5(skillsDir, entry.name, "SKILL.md");
|
|
998
|
+
if (!existsSync5(skillFile)) continue;
|
|
999
|
+
const raw = readFileSync5(skillFile, "utf-8");
|
|
1000
|
+
const { metadata, content } = parseSkillFrontmatter(raw);
|
|
1001
|
+
const description = typeof metadata.description === "string" ? metadata.description : "";
|
|
1002
|
+
const skill = {
|
|
1003
|
+
name: entry.name,
|
|
1004
|
+
description,
|
|
1005
|
+
skillContent: content,
|
|
1006
|
+
...metadata
|
|
1007
|
+
};
|
|
1008
|
+
skills.push(skill);
|
|
1009
|
+
}
|
|
1010
|
+
return skills;
|
|
1011
|
+
}
|
|
1012
|
+
/** Load commands from the plugin's commands/ directory (flat .md files). */
|
|
1013
|
+
loadCommands(pluginDir, pluginName) {
|
|
1014
|
+
const commandsDir = join5(pluginDir, "commands");
|
|
1015
|
+
if (!existsSync5(commandsDir)) return [];
|
|
1016
|
+
const entries = readdirSync(commandsDir, { withFileTypes: true });
|
|
1017
|
+
const commands = [];
|
|
1018
|
+
for (const entry of entries) {
|
|
1019
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1020
|
+
const raw = readFileSync5(join5(commandsDir, entry.name), "utf-8");
|
|
1021
|
+
const { metadata, content } = parseSkillFrontmatter(raw);
|
|
1022
|
+
const name = typeof metadata.name === "string" ? metadata.name : entry.name.replace(/\.md$/, "");
|
|
1023
|
+
const description = typeof metadata.description === "string" ? metadata.description : "";
|
|
1024
|
+
commands.push({
|
|
1025
|
+
...metadata,
|
|
1026
|
+
name: `${pluginName}:${name}`,
|
|
1027
|
+
description,
|
|
1028
|
+
skillContent: content
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
return commands;
|
|
1032
|
+
}
|
|
1033
|
+
/** Load hooks from hooks/hooks.json if present. */
|
|
1034
|
+
loadHooks(pluginDir) {
|
|
1035
|
+
const hooksPath = join5(pluginDir, "hooks", "hooks.json");
|
|
1036
|
+
if (!existsSync5(hooksPath)) return {};
|
|
1037
|
+
try {
|
|
1038
|
+
const raw = readFileSync5(hooksPath, "utf-8");
|
|
1039
|
+
const data = JSON.parse(raw);
|
|
1040
|
+
if (typeof data === "object" && data !== null) {
|
|
1041
|
+
return data;
|
|
1042
|
+
}
|
|
1043
|
+
return {};
|
|
1044
|
+
} catch {
|
|
1045
|
+
return {};
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
/** Load MCP server configuration if present. Checks `.mcp.json` at plugin root first. */
|
|
1049
|
+
loadMcpConfig(pluginDir) {
|
|
1050
|
+
const primaryPath = join5(pluginDir, ".mcp.json");
|
|
1051
|
+
const fallbackPath = join5(pluginDir, ".claude-plugin", "mcp.json");
|
|
1052
|
+
const mcpPath = existsSync5(primaryPath) ? primaryPath : fallbackPath;
|
|
1053
|
+
if (!existsSync5(mcpPath)) return void 0;
|
|
1054
|
+
try {
|
|
1055
|
+
const raw = readFileSync5(mcpPath, "utf-8");
|
|
1056
|
+
return JSON.parse(raw);
|
|
1057
|
+
} catch {
|
|
1058
|
+
return void 0;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/** Load agent definitions from agents/ directory if present. */
|
|
1062
|
+
loadAgents(pluginDir) {
|
|
1063
|
+
const agentsDir = join5(pluginDir, "agents");
|
|
1064
|
+
if (!existsSync5(agentsDir)) return [];
|
|
1065
|
+
try {
|
|
1066
|
+
const entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
1067
|
+
return entries.filter((e) => e.isDirectory() || e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, ""));
|
|
1068
|
+
} catch {
|
|
1069
|
+
return [];
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
445
1073
|
|
|
446
|
-
// src/
|
|
447
|
-
import {
|
|
1074
|
+
// src/plugins/bundle-plugin-installer.ts
|
|
1075
|
+
import { execSync } from "child_process";
|
|
1076
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1077
|
+
import { join as join6, dirname as dirname3 } from "path";
|
|
1078
|
+
var GIT_CLONE_TIMEOUT_MS = 6e4;
|
|
1079
|
+
var BundlePluginInstaller = class {
|
|
1080
|
+
pluginsDir;
|
|
1081
|
+
cacheDir;
|
|
1082
|
+
registryPath;
|
|
1083
|
+
settingsStore;
|
|
1084
|
+
marketplaceClient;
|
|
1085
|
+
exec;
|
|
1086
|
+
constructor(options) {
|
|
1087
|
+
this.pluginsDir = options.pluginsDir;
|
|
1088
|
+
this.cacheDir = join6(this.pluginsDir, "cache");
|
|
1089
|
+
this.registryPath = join6(this.pluginsDir, "installed_plugins.json");
|
|
1090
|
+
this.settingsStore = options.settingsStore;
|
|
1091
|
+
this.marketplaceClient = options.marketplaceClient;
|
|
1092
|
+
this.exec = options.exec ?? this.defaultExec;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Install a plugin from a marketplace.
|
|
1096
|
+
*
|
|
1097
|
+
* 1. Read marketplace manifest to find the plugin entry.
|
|
1098
|
+
* 2. Resolve source (relative path, github, or url).
|
|
1099
|
+
* 3. Copy/clone to `cache/<marketplace>/<plugin>/<version>/`.
|
|
1100
|
+
* 4. Record in `installed_plugins.json`.
|
|
1101
|
+
*/
|
|
1102
|
+
async install(pluginName, marketplaceName) {
|
|
1103
|
+
const manifest = this.marketplaceClient.fetchManifest(marketplaceName);
|
|
1104
|
+
const entry = manifest.plugins.find((p) => p.name === pluginName);
|
|
1105
|
+
if (!entry) {
|
|
1106
|
+
throw new Error(`Plugin "${pluginName}" not found in marketplace "${marketplaceName}"`);
|
|
1107
|
+
}
|
|
1108
|
+
const version = this.resolveVersion(entry, marketplaceName);
|
|
1109
|
+
const targetDir = join6(this.cacheDir, marketplaceName, pluginName, version);
|
|
1110
|
+
if (existsSync6(targetDir)) {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`Plugin "${pluginName}" version "${version}" is already installed from "${marketplaceName}"`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
this.resolveAndInstall(entry.source, marketplaceName, pluginName, targetDir);
|
|
1116
|
+
const pluginId = `${pluginName}@${marketplaceName}`;
|
|
1117
|
+
const registry = this.readRegistry();
|
|
1118
|
+
registry[pluginId] = {
|
|
1119
|
+
pluginName,
|
|
1120
|
+
marketplace: marketplaceName,
|
|
1121
|
+
version,
|
|
1122
|
+
installPath: targetDir,
|
|
1123
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1124
|
+
};
|
|
1125
|
+
this.writeRegistry(registry);
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Uninstall a plugin.
|
|
1129
|
+
* Removes from cache and from installed_plugins.json.
|
|
1130
|
+
*/
|
|
1131
|
+
async uninstall(pluginId) {
|
|
1132
|
+
const registry = this.readRegistry();
|
|
1133
|
+
const record = registry[pluginId];
|
|
1134
|
+
if (!record) {
|
|
1135
|
+
throw new Error(`Plugin "${pluginId}" is not installed`);
|
|
1136
|
+
}
|
|
1137
|
+
if (existsSync6(record.installPath)) {
|
|
1138
|
+
rmSync(record.installPath, { recursive: true, force: true });
|
|
1139
|
+
}
|
|
1140
|
+
delete registry[pluginId];
|
|
1141
|
+
this.writeRegistry(registry);
|
|
1142
|
+
this.settingsStore.removePluginEntry(pluginId);
|
|
1143
|
+
}
|
|
1144
|
+
/** Enable a plugin by setting its enabledPlugins entry to true. */
|
|
1145
|
+
async enable(pluginId) {
|
|
1146
|
+
this.settingsStore.setPluginEnabled(pluginId, true);
|
|
1147
|
+
}
|
|
1148
|
+
/** Disable a plugin by setting its enabledPlugins entry to false. */
|
|
1149
|
+
async disable(pluginId) {
|
|
1150
|
+
this.settingsStore.setPluginEnabled(pluginId, false);
|
|
1151
|
+
}
|
|
1152
|
+
/** Get all installed plugins. */
|
|
1153
|
+
getInstalledPlugins() {
|
|
1154
|
+
return this.readRegistry();
|
|
1155
|
+
}
|
|
1156
|
+
/** Get plugins installed from a specific marketplace. */
|
|
1157
|
+
getPluginsByMarketplace(marketplaceName) {
|
|
1158
|
+
const registry = this.readRegistry();
|
|
1159
|
+
return Object.values(registry).filter((r) => r.marketplace === marketplaceName);
|
|
1160
|
+
}
|
|
1161
|
+
// --- Private helpers ---
|
|
1162
|
+
/** Resolve the version for a plugin entry. */
|
|
1163
|
+
resolveVersion(entry, marketplaceName) {
|
|
1164
|
+
const entryWithVersion = entry;
|
|
1165
|
+
if (typeof entryWithVersion.version === "string" && entryWithVersion.version) {
|
|
1166
|
+
return entryWithVersion.version;
|
|
1167
|
+
}
|
|
1168
|
+
return this.marketplaceClient.getMarketplaceSha(marketplaceName);
|
|
1169
|
+
}
|
|
1170
|
+
/** Resolve the source and install the plugin. */
|
|
1171
|
+
resolveAndInstall(source, marketplaceName, pluginName, targetDir) {
|
|
1172
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
1173
|
+
if (typeof source === "string") {
|
|
1174
|
+
const marketplaceDir = this.marketplaceClient.getMarketplaceDir(marketplaceName);
|
|
1175
|
+
const sourcePath = join6(marketplaceDir, source);
|
|
1176
|
+
if (!existsSync6(sourcePath)) {
|
|
1177
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
`Plugin source path "${source}" not found in marketplace "${marketplaceName}"`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
cpSync(sourcePath, targetDir, { recursive: true });
|
|
1183
|
+
} else if (source.type === "github") {
|
|
1184
|
+
const repoUrl = `https://github.com/${source.repo}.git`;
|
|
1185
|
+
this.cloneToDir(repoUrl, targetDir, pluginName);
|
|
1186
|
+
} else if (source.type === "url") {
|
|
1187
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
1188
|
+
throw new Error("URL source installation is not yet supported");
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
/** Clone a git repository to the target directory. */
|
|
1192
|
+
cloneToDir(repoUrl, targetDir, pluginName) {
|
|
1193
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
1194
|
+
const command = `git clone --depth 1 ${repoUrl} ${targetDir}`;
|
|
1195
|
+
try {
|
|
1196
|
+
this.exec(command, { timeout: GIT_CLONE_TIMEOUT_MS, stdio: "pipe" });
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1199
|
+
throw new Error(`Failed to clone plugin "${pluginName}": ${message}`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/** Read the installed_plugins.json registry. */
|
|
1203
|
+
readRegistry() {
|
|
1204
|
+
if (!existsSync6(this.registryPath)) {
|
|
1205
|
+
return {};
|
|
1206
|
+
}
|
|
1207
|
+
try {
|
|
1208
|
+
const raw = readFileSync6(this.registryPath, "utf-8");
|
|
1209
|
+
const data = JSON.parse(raw);
|
|
1210
|
+
if (typeof data === "object" && data !== null) {
|
|
1211
|
+
return data;
|
|
1212
|
+
}
|
|
1213
|
+
return {};
|
|
1214
|
+
} catch {
|
|
1215
|
+
return {};
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/** Write the installed_plugins.json registry. */
|
|
1219
|
+
writeRegistry(registry) {
|
|
1220
|
+
const dir = dirname3(this.registryPath);
|
|
1221
|
+
if (!existsSync6(dir)) {
|
|
1222
|
+
mkdirSync2(dir, { recursive: true });
|
|
1223
|
+
}
|
|
1224
|
+
writeFileSync2(this.registryPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1225
|
+
}
|
|
1226
|
+
/** Default exec implementation using child_process. */
|
|
1227
|
+
defaultExec(command, options) {
|
|
1228
|
+
return execSync(command, { timeout: options.timeout, stdio: "pipe" });
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
448
1231
|
|
|
449
|
-
// src/
|
|
450
|
-
import {
|
|
1232
|
+
// src/plugins/marketplace-client.ts
|
|
1233
|
+
import { execSync as execSync2 } from "child_process";
|
|
1234
|
+
import {
|
|
1235
|
+
cpSync as cpSync2,
|
|
1236
|
+
existsSync as existsSync7,
|
|
1237
|
+
mkdirSync as mkdirSync3,
|
|
1238
|
+
readFileSync as readFileSync7,
|
|
1239
|
+
renameSync,
|
|
1240
|
+
rmSync as rmSync2,
|
|
1241
|
+
writeFileSync as writeFileSync3
|
|
1242
|
+
} from "fs";
|
|
1243
|
+
import { join as join7, dirname as dirname4 } from "path";
|
|
1244
|
+
var GIT_TIMEOUT_MS = 6e4;
|
|
1245
|
+
var MarketplaceClient = class {
|
|
1246
|
+
pluginsDir;
|
|
1247
|
+
exec;
|
|
1248
|
+
marketplacesDir;
|
|
1249
|
+
registryPath;
|
|
1250
|
+
constructor(options) {
|
|
1251
|
+
this.pluginsDir = options.pluginsDir;
|
|
1252
|
+
this.exec = options.exec ?? this.defaultExec;
|
|
1253
|
+
this.marketplacesDir = join7(this.pluginsDir, "marketplaces");
|
|
1254
|
+
this.registryPath = join7(this.pluginsDir, "known_marketplaces.json");
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Add a marketplace by cloning its repository.
|
|
1258
|
+
*
|
|
1259
|
+
* 1. Parse source: `owner/repo` string becomes a GitHub source.
|
|
1260
|
+
* 2. Shallow git clone (`--depth 1`) to `marketplaces/<name>/`.
|
|
1261
|
+
* 3. Read `.claude-plugin/marketplace.json` for the `name` field.
|
|
1262
|
+
* 4. Register in `known_marketplaces.json`.
|
|
1263
|
+
*
|
|
1264
|
+
* Returns the registered marketplace name from the manifest.
|
|
1265
|
+
*/
|
|
1266
|
+
addMarketplace(source) {
|
|
1267
|
+
const tempName = "temp-" + Date.now().toString(36);
|
|
1268
|
+
const tempDir = join7(this.marketplacesDir, tempName);
|
|
1269
|
+
mkdirSync3(this.marketplacesDir, { recursive: true });
|
|
1270
|
+
if (source.type === "local") {
|
|
1271
|
+
if (!existsSync7(source.path)) {
|
|
1272
|
+
throw new Error(`Local marketplace path does not exist: ${source.path}`);
|
|
1273
|
+
}
|
|
1274
|
+
cpSync2(source.path, tempDir, { recursive: true });
|
|
1275
|
+
} else {
|
|
1276
|
+
const cloneUrl = this.resolveCloneUrl(source);
|
|
1277
|
+
const command = `git clone --depth 1 ${cloneUrl} ${tempDir}`;
|
|
1278
|
+
try {
|
|
1279
|
+
this.exec(command, { timeout: GIT_TIMEOUT_MS, stdio: "pipe" });
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1282
|
+
throw new Error(`Failed to clone marketplace: ${message}`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const manifestPath = join7(tempDir, ".claude-plugin", "marketplace.json");
|
|
1286
|
+
if (!existsSync7(manifestPath)) {
|
|
1287
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1288
|
+
throw new Error(
|
|
1289
|
+
source.type === "local" ? "Local directory does not contain .claude-plugin/marketplace.json" : "Cloned repository does not contain .claude-plugin/marketplace.json"
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
const manifest = this.readManifestFromPath(manifestPath);
|
|
1293
|
+
const name = manifest.name;
|
|
1294
|
+
if (!name) {
|
|
1295
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1296
|
+
throw new Error('Marketplace manifest does not contain a "name" field');
|
|
1297
|
+
}
|
|
1298
|
+
const registry = this.readRegistry();
|
|
1299
|
+
if (registry[name]) {
|
|
1300
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1301
|
+
throw new Error(`Marketplace "${name}" already exists`);
|
|
1302
|
+
}
|
|
1303
|
+
const finalDir = join7(this.marketplacesDir, name);
|
|
1304
|
+
renameSync(tempDir, finalDir);
|
|
1305
|
+
registry[name] = {
|
|
1306
|
+
source,
|
|
1307
|
+
installLocation: finalDir,
|
|
1308
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1309
|
+
};
|
|
1310
|
+
this.writeRegistry(registry);
|
|
1311
|
+
return name;
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Remove a marketplace.
|
|
1315
|
+
* Uninstalls all plugins from that marketplace, then deletes the clone directory
|
|
1316
|
+
* and removes from the registry.
|
|
1317
|
+
*/
|
|
1318
|
+
removeMarketplace(name) {
|
|
1319
|
+
const registry = this.readRegistry();
|
|
1320
|
+
const entry = registry[name];
|
|
1321
|
+
if (!entry) {
|
|
1322
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1323
|
+
}
|
|
1324
|
+
this.removeInstalledPluginsForMarketplace(name);
|
|
1325
|
+
if (existsSync7(entry.installLocation)) {
|
|
1326
|
+
rmSync2(entry.installLocation, { recursive: true, force: true });
|
|
1327
|
+
}
|
|
1328
|
+
delete registry[name];
|
|
1329
|
+
this.writeRegistry(registry);
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Update a marketplace by running git pull on its clone.
|
|
1333
|
+
* The manifest is re-read from disk on demand (via fetchManifest), so the
|
|
1334
|
+
* updated manifest is automatically available after pull.
|
|
1335
|
+
*
|
|
1336
|
+
* TODO: After pull, detect version changes in installed plugins and offer
|
|
1337
|
+
* to update them (re-install at new version).
|
|
1338
|
+
*/
|
|
1339
|
+
updateMarketplace(name) {
|
|
1340
|
+
const registry = this.readRegistry();
|
|
1341
|
+
const entry = registry[name];
|
|
1342
|
+
if (!entry) {
|
|
1343
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1344
|
+
}
|
|
1345
|
+
if (!existsSync7(entry.installLocation)) {
|
|
1346
|
+
throw new Error(`Marketplace directory for "${name}" does not exist`);
|
|
1347
|
+
}
|
|
1348
|
+
if (entry.source.type === "local") {
|
|
1349
|
+
const localSource = entry.source;
|
|
1350
|
+
if (!existsSync7(localSource.path)) {
|
|
1351
|
+
throw new Error(`Local marketplace path does not exist: ${localSource.path}`);
|
|
1352
|
+
}
|
|
1353
|
+
rmSync2(entry.installLocation, { recursive: true, force: true });
|
|
1354
|
+
cpSync2(localSource.path, entry.installLocation, { recursive: true });
|
|
1355
|
+
} else {
|
|
1356
|
+
const command = `git -C ${entry.installLocation} pull`;
|
|
1357
|
+
try {
|
|
1358
|
+
this.exec(command, { timeout: GIT_TIMEOUT_MS, stdio: "pipe" });
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1361
|
+
throw new Error(`Failed to update marketplace "${name}": ${message}`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
entry.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1365
|
+
this.writeRegistry(registry);
|
|
1366
|
+
}
|
|
1367
|
+
/** List all registered marketplaces. */
|
|
1368
|
+
listMarketplaces() {
|
|
1369
|
+
const registry = this.readRegistry();
|
|
1370
|
+
return Object.entries(registry).map(([name, entry]) => ({
|
|
1371
|
+
name,
|
|
1372
|
+
source: entry.source,
|
|
1373
|
+
lastUpdated: entry.lastUpdated
|
|
1374
|
+
}));
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Read the marketplace manifest from a registered marketplace's clone.
|
|
1378
|
+
*/
|
|
1379
|
+
fetchManifest(marketplaceName) {
|
|
1380
|
+
const registry = this.readRegistry();
|
|
1381
|
+
const entry = registry[marketplaceName];
|
|
1382
|
+
if (!entry) {
|
|
1383
|
+
throw new Error(`Marketplace "${marketplaceName}" not found`);
|
|
1384
|
+
}
|
|
1385
|
+
const manifestPath = join7(entry.installLocation, ".claude-plugin", "marketplace.json");
|
|
1386
|
+
if (!existsSync7(manifestPath)) {
|
|
1387
|
+
throw new Error(
|
|
1388
|
+
`Marketplace "${marketplaceName}" does not contain .claude-plugin/marketplace.json`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
return this.readManifestFromPath(manifestPath);
|
|
1392
|
+
}
|
|
1393
|
+
/** Get the clone directory path for a registered marketplace. */
|
|
1394
|
+
getMarketplaceDir(name) {
|
|
1395
|
+
const registry = this.readRegistry();
|
|
1396
|
+
const entry = registry[name];
|
|
1397
|
+
if (!entry) {
|
|
1398
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1399
|
+
}
|
|
1400
|
+
return entry.installLocation;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Get the current git SHA (first 12 chars) for a marketplace clone.
|
|
1404
|
+
* Used as a version identifier when plugins lack explicit versions.
|
|
1405
|
+
*/
|
|
1406
|
+
getMarketplaceSha(name) {
|
|
1407
|
+
const dir = this.getMarketplaceDir(name);
|
|
1408
|
+
try {
|
|
1409
|
+
const result = this.exec(`git -C ${dir} rev-parse HEAD`, {
|
|
1410
|
+
timeout: GIT_TIMEOUT_MS,
|
|
1411
|
+
stdio: "pipe"
|
|
1412
|
+
});
|
|
1413
|
+
return result.toString().trim().slice(0, 12);
|
|
1414
|
+
} catch {
|
|
1415
|
+
return "unknown";
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
/** List all available plugins across all marketplaces. */
|
|
1419
|
+
listAvailablePlugins() {
|
|
1420
|
+
const results = [];
|
|
1421
|
+
const marketplaces = this.listMarketplaces();
|
|
1422
|
+
for (const { name } of marketplaces) {
|
|
1423
|
+
try {
|
|
1424
|
+
const manifest = this.fetchManifest(name);
|
|
1425
|
+
for (const plugin of manifest.plugins) {
|
|
1426
|
+
results.push({ ...plugin, marketplace: name });
|
|
1427
|
+
}
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return results;
|
|
1432
|
+
}
|
|
1433
|
+
// --- Private helpers ---
|
|
1434
|
+
/** Resolve a marketplace source to a git clone URL. */
|
|
1435
|
+
resolveCloneUrl(source) {
|
|
1436
|
+
switch (source.type) {
|
|
1437
|
+
case "github":
|
|
1438
|
+
return `https://github.com/${source.repo}.git`;
|
|
1439
|
+
case "git":
|
|
1440
|
+
return source.url;
|
|
1441
|
+
case "local":
|
|
1442
|
+
throw new Error("Local source type does not use git cloning");
|
|
1443
|
+
case "url":
|
|
1444
|
+
throw new Error("URL marketplace source is not yet supported");
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Remove all installed plugins that belong to a given marketplace.
|
|
1449
|
+
* Reads installed_plugins.json, deletes cache directories for matching plugins,
|
|
1450
|
+
* and updates the registry.
|
|
1451
|
+
*/
|
|
1452
|
+
removeInstalledPluginsForMarketplace(marketplaceName) {
|
|
1453
|
+
const installedPath = join7(this.pluginsDir, "installed_plugins.json");
|
|
1454
|
+
if (!existsSync7(installedPath)) return;
|
|
1455
|
+
let registry;
|
|
1456
|
+
try {
|
|
1457
|
+
const raw = readFileSync7(installedPath, "utf-8");
|
|
1458
|
+
const data = JSON.parse(raw);
|
|
1459
|
+
if (typeof data !== "object" || data === null) return;
|
|
1460
|
+
registry = data;
|
|
1461
|
+
} catch {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
let changed = false;
|
|
1465
|
+
for (const [pluginId, record] of Object.entries(registry)) {
|
|
1466
|
+
if (record.marketplace === marketplaceName) {
|
|
1467
|
+
if (record.installPath && existsSync7(record.installPath)) {
|
|
1468
|
+
rmSync2(record.installPath, { recursive: true, force: true });
|
|
1469
|
+
}
|
|
1470
|
+
delete registry[pluginId];
|
|
1471
|
+
changed = true;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (changed) {
|
|
1475
|
+
const dir = dirname4(installedPath);
|
|
1476
|
+
if (!existsSync7(dir)) {
|
|
1477
|
+
mkdirSync3(dir, { recursive: true });
|
|
1478
|
+
}
|
|
1479
|
+
writeFileSync3(installedPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
/** Read and parse a marketplace.json from a file path. */
|
|
1483
|
+
readManifestFromPath(path) {
|
|
1484
|
+
const raw = readFileSync7(path, "utf-8");
|
|
1485
|
+
const data = JSON.parse(raw);
|
|
1486
|
+
if (typeof data !== "object" || data === null) {
|
|
1487
|
+
throw new Error("Invalid marketplace manifest: not an object");
|
|
1488
|
+
}
|
|
1489
|
+
const obj = data;
|
|
1490
|
+
if (typeof obj.name !== "string") {
|
|
1491
|
+
throw new Error('Invalid marketplace manifest: missing "name" field');
|
|
1492
|
+
}
|
|
1493
|
+
return data;
|
|
1494
|
+
}
|
|
1495
|
+
/** Read the known_marketplaces.json registry. */
|
|
1496
|
+
readRegistry() {
|
|
1497
|
+
if (!existsSync7(this.registryPath)) {
|
|
1498
|
+
return {};
|
|
1499
|
+
}
|
|
1500
|
+
try {
|
|
1501
|
+
const raw = readFileSync7(this.registryPath, "utf-8");
|
|
1502
|
+
const data = JSON.parse(raw);
|
|
1503
|
+
if (typeof data === "object" && data !== null) {
|
|
1504
|
+
return data;
|
|
1505
|
+
}
|
|
1506
|
+
return {};
|
|
1507
|
+
} catch {
|
|
1508
|
+
return {};
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/** Write the known_marketplaces.json registry. */
|
|
1512
|
+
writeRegistry(registry) {
|
|
1513
|
+
const dir = dirname4(this.registryPath);
|
|
1514
|
+
if (!existsSync7(dir)) {
|
|
1515
|
+
mkdirSync3(dir, { recursive: true });
|
|
1516
|
+
}
|
|
1517
|
+
writeFileSync3(this.registryPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1518
|
+
}
|
|
1519
|
+
/** Default exec implementation using child_process. */
|
|
1520
|
+
defaultExec(command, options) {
|
|
1521
|
+
return execSync2(command, { timeout: options.timeout, stdio: "pipe" });
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
451
1524
|
|
|
452
1525
|
// src/tools/agent-tool.ts
|
|
453
1526
|
import { z as z2 } from "zod";
|
|
454
1527
|
import { createZodFunctionTool } from "@robota-sdk/agent-tools";
|
|
455
|
-
import { Session as Session3 } from "@robota-sdk/agent-sessions";
|
|
456
1528
|
function asZodSchema(schema) {
|
|
457
1529
|
return schema;
|
|
458
1530
|
}
|
|
@@ -473,26 +1545,26 @@ async function runAgent(args) {
|
|
|
473
1545
|
};
|
|
474
1546
|
return JSON.stringify(result);
|
|
475
1547
|
}
|
|
476
|
-
const
|
|
1548
|
+
const noopTerminal = {
|
|
1549
|
+
write: () => {
|
|
1550
|
+
},
|
|
1551
|
+
writeLine: () => {
|
|
1552
|
+
},
|
|
1553
|
+
writeMarkdown: () => {
|
|
1554
|
+
},
|
|
1555
|
+
writeError: () => {
|
|
1556
|
+
},
|
|
1557
|
+
prompt: () => Promise.resolve(""),
|
|
1558
|
+
select: () => Promise.resolve(0),
|
|
1559
|
+
spinner: () => ({ stop: () => {
|
|
1560
|
+
}, update: () => {
|
|
1561
|
+
} })
|
|
1562
|
+
};
|
|
1563
|
+
const subSession = createSession({
|
|
477
1564
|
config: agentToolDeps.config,
|
|
478
1565
|
context: agentToolDeps.context,
|
|
479
1566
|
projectInfo: agentToolDeps.projectInfo,
|
|
480
|
-
|
|
481
|
-
terminal: {
|
|
482
|
-
write: () => {
|
|
483
|
-
},
|
|
484
|
-
writeLine: () => {
|
|
485
|
-
},
|
|
486
|
-
writeMarkdown: () => {
|
|
487
|
-
},
|
|
488
|
-
writeError: () => {
|
|
489
|
-
},
|
|
490
|
-
prompt: () => Promise.resolve(""),
|
|
491
|
-
select: () => Promise.resolve(0),
|
|
492
|
-
spinner: () => ({ stop: () => {
|
|
493
|
-
}, update: () => {
|
|
494
|
-
} })
|
|
495
|
-
},
|
|
1567
|
+
terminal: noopTerminal,
|
|
496
1568
|
// Sub-agents bypass permissions — they inherit parent's trust
|
|
497
1569
|
permissionMode: "bypassPermissions"
|
|
498
1570
|
});
|
|
@@ -521,28 +1593,46 @@ var agentTool = createZodFunctionTool(
|
|
|
521
1593
|
return runAgent(params);
|
|
522
1594
|
}
|
|
523
1595
|
);
|
|
1596
|
+
|
|
1597
|
+
// src/index.ts
|
|
1598
|
+
import { bashTool as bashTool2 } from "@robota-sdk/agent-tools";
|
|
1599
|
+
import { readTool as readTool2 } from "@robota-sdk/agent-tools";
|
|
1600
|
+
import { writeTool as writeTool2 } from "@robota-sdk/agent-tools";
|
|
1601
|
+
import { editTool as editTool2 } from "@robota-sdk/agent-tools";
|
|
1602
|
+
import { globTool as globTool2 } from "@robota-sdk/agent-tools";
|
|
1603
|
+
import { grepTool as grepTool2 } from "@robota-sdk/agent-tools";
|
|
524
1604
|
export {
|
|
1605
|
+
AgentExecutor,
|
|
1606
|
+
BundlePluginInstaller,
|
|
1607
|
+
BundlePluginLoader,
|
|
1608
|
+
DEFAULT_TOOL_DESCRIPTIONS,
|
|
525
1609
|
FileSessionLogger,
|
|
526
|
-
|
|
1610
|
+
MarketplaceClient,
|
|
1611
|
+
PluginSettingsStore,
|
|
1612
|
+
PromptExecutor,
|
|
1613
|
+
Session2 as Session,
|
|
527
1614
|
SessionStore,
|
|
528
1615
|
SilentSessionLogger,
|
|
529
1616
|
TRUST_TO_MODE,
|
|
530
1617
|
agentTool,
|
|
531
|
-
bashTool,
|
|
1618
|
+
bashTool2 as bashTool,
|
|
532
1619
|
buildSystemPrompt,
|
|
1620
|
+
createDefaultTools,
|
|
1621
|
+
createProvider,
|
|
1622
|
+
createSession,
|
|
533
1623
|
detectProject,
|
|
534
|
-
editTool,
|
|
1624
|
+
editTool2 as editTool,
|
|
535
1625
|
evaluatePermission,
|
|
536
|
-
globTool,
|
|
537
|
-
grepTool,
|
|
1626
|
+
globTool2 as globTool,
|
|
1627
|
+
grepTool2 as grepTool,
|
|
538
1628
|
loadConfig,
|
|
539
1629
|
loadContext,
|
|
540
1630
|
projectPaths,
|
|
541
1631
|
promptForApproval,
|
|
542
1632
|
query,
|
|
543
|
-
readTool,
|
|
1633
|
+
readTool2 as readTool,
|
|
544
1634
|
runHooks,
|
|
545
1635
|
setAgentToolDeps,
|
|
546
1636
|
userPaths,
|
|
547
|
-
writeTool
|
|
1637
|
+
writeTool2 as writeTool
|
|
548
1638
|
};
|