@robota-sdk/agent-sdk 3.0.0-beta.4 → 3.0.0-beta.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +322 -21
- package/dist/node/index.cjs +2205 -139
- package/dist/node/index.d.cts +897 -178
- package/dist/node/index.d.ts +897 -178
- package/dist/node/index.js +2213 -157
- package/package.json +5 -5
package/dist/node/index.js
CHANGED
|
@@ -1,8 +1,115 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
2
|
import { TRUST_TO_MODE } from "@robota-sdk/agent-core";
|
|
3
3
|
|
|
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
|
+
|
|
4
111
|
// src/assembly/create-session.ts
|
|
5
|
-
import { Session } from "@robota-sdk/agent-sessions";
|
|
112
|
+
import { Session as Session2 } from "@robota-sdk/agent-sessions";
|
|
6
113
|
|
|
7
114
|
// src/context/system-prompt-builder.ts
|
|
8
115
|
var TRUST_LEVEL_DESCRIPTIONS = {
|
|
@@ -33,17 +140,38 @@ function buildToolsSection(descriptions) {
|
|
|
33
140
|
const lines = ["## Available Tools", ...descriptions.map((d) => `- ${d}`)];
|
|
34
141
|
return lines.join("\n");
|
|
35
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
|
+
}
|
|
36
156
|
function buildSystemPrompt(params) {
|
|
37
|
-
const { agentsMd, claudeMd, toolDescriptions, trustLevel, projectInfo } = params;
|
|
157
|
+
const { agentsMd, claudeMd, toolDescriptions, trustLevel, projectInfo, cwd, language } = params;
|
|
38
158
|
const sections = [];
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
|
47
175
|
sections.push(buildProjectSection(projectInfo));
|
|
48
176
|
sections.push(
|
|
49
177
|
[
|
|
@@ -69,6 +197,12 @@ function buildSystemPrompt(params) {
|
|
|
69
197
|
if (toolsSection.length > 0) {
|
|
70
198
|
sections.push(toolsSection);
|
|
71
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
|
+
}
|
|
72
206
|
return sections.join("\n\n");
|
|
73
207
|
}
|
|
74
208
|
|
|
@@ -117,25 +251,407 @@ function createProvider(config) {
|
|
|
117
251
|
return new AnthropicProvider({ apiKey });
|
|
118
252
|
}
|
|
119
253
|
|
|
254
|
+
// src/tools/agent-tool.ts
|
|
255
|
+
import { z } from "zod";
|
|
256
|
+
import { createZodFunctionTool } from "@robota-sdk/agent-tools";
|
|
257
|
+
|
|
258
|
+
// src/agents/built-in-agents.ts
|
|
259
|
+
var GENERAL_PURPOSE_SYSTEM_PROMPT = `You are a general-purpose task execution agent. You have access to all tools available in the parent session and can perform any task delegated to you.
|
|
260
|
+
|
|
261
|
+
Your role is to complete the assigned task thoroughly and accurately. Follow these guidelines:
|
|
262
|
+
|
|
263
|
+
- Execute the task as described in the prompt. Do not expand scope beyond what is requested.
|
|
264
|
+
- Use the most appropriate tools for each step. Prefer precise tools (Read, Grep, Glob) over broad ones (Bash) when possible.
|
|
265
|
+
- Report your findings clearly and concisely when the task is complete.
|
|
266
|
+
- If a task cannot be completed, explain why and what information is missing.
|
|
267
|
+
- Maintain the same code quality standards as the parent session (strict types, no fallbacks, proper error handling).`;
|
|
268
|
+
var EXPLORE_SYSTEM_PROMPT = `You are a codebase exploration and analysis agent. Your purpose is to search, read, and understand code without making any modifications.
|
|
269
|
+
|
|
270
|
+
You operate in read-only mode. You must NEVER attempt to write or edit files. Your tools are restricted to read-only operations: reading files, searching with grep and glob, and running non-destructive bash commands.
|
|
271
|
+
|
|
272
|
+
Your role is to answer questions about the codebase by:
|
|
273
|
+
|
|
274
|
+
- Searching for relevant files, symbols, and patterns using Glob and Grep.
|
|
275
|
+
- Reading source files, configuration, and documentation to understand structure and behavior.
|
|
276
|
+
- Tracing code paths across modules to understand how components interact.
|
|
277
|
+
- Summarizing findings in a clear, structured format with file paths and line references.
|
|
278
|
+
- Identifying architectural patterns, dependencies, and potential issues.
|
|
279
|
+
|
|
280
|
+
When exploring, prefer targeted searches over broad scans. Start with the most likely locations and narrow down. Always include absolute file paths in your responses so the caller can navigate directly to relevant code.`;
|
|
281
|
+
var PLAN_SYSTEM_PROMPT = `You are a planning, research, and architecture agent. Your purpose is to analyze requirements, research approaches, and produce structured plans without making any code modifications.
|
|
282
|
+
|
|
283
|
+
You operate in read-only mode. You must NEVER attempt to write or edit files. Your tools are restricted to read-only operations.
|
|
284
|
+
|
|
285
|
+
Your role is to:
|
|
286
|
+
|
|
287
|
+
- Analyze the current codebase state relevant to the task by reading specs, source code, and tests.
|
|
288
|
+
- Research implementation approaches by examining existing patterns and architectural conventions in the repository.
|
|
289
|
+
- Identify affected files, modules, and interfaces that a proposed change would touch.
|
|
290
|
+
- Assess risks, dependencies, and potential breaking changes.
|
|
291
|
+
- Produce a structured implementation plan with clear steps, file lists, and ordering.
|
|
292
|
+
- Consider edge cases, error handling, and test coverage requirements.
|
|
293
|
+
|
|
294
|
+
Output your plan in a structured format with numbered steps. For each step, specify which files are involved and what changes are needed. Flag any decisions that require human judgment or clarification.`;
|
|
295
|
+
var BUILT_IN_AGENTS = [
|
|
296
|
+
{
|
|
297
|
+
name: "general-purpose",
|
|
298
|
+
description: "General-purpose task execution agent with full tool access.",
|
|
299
|
+
systemPrompt: GENERAL_PURPOSE_SYSTEM_PROMPT
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: "Explore",
|
|
303
|
+
description: "Read-only codebase exploration and analysis agent.",
|
|
304
|
+
systemPrompt: EXPLORE_SYSTEM_PROMPT,
|
|
305
|
+
model: "claude-haiku-4-5",
|
|
306
|
+
disallowedTools: ["Write", "Edit"]
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "Plan",
|
|
310
|
+
description: "Read-only planning, research, and architecture agent.",
|
|
311
|
+
systemPrompt: PLAN_SYSTEM_PROMPT,
|
|
312
|
+
disallowedTools: ["Write", "Edit"]
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
function getBuiltInAgent(name) {
|
|
316
|
+
return BUILT_IN_AGENTS.find((agent) => agent.name === name);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/assembly/create-subagent-session.ts
|
|
320
|
+
import { Session } from "@robota-sdk/agent-sessions";
|
|
321
|
+
|
|
322
|
+
// src/assembly/subagent-prompts.ts
|
|
323
|
+
function getSubagentSuffix() {
|
|
324
|
+
return `When you complete the task, respond with a concise report covering what was done and any key findings \u2014 the caller will relay this to the user, so it only needs the essentials.
|
|
325
|
+
|
|
326
|
+
In your final response, share file paths (always absolute, never relative) that are relevant to the task. Include code snippets only when the exact text is load-bearing \u2014 do not recap code you merely read.
|
|
327
|
+
|
|
328
|
+
Do not use emojis.`;
|
|
329
|
+
}
|
|
330
|
+
function getForkWorkerSuffix() {
|
|
331
|
+
return `You are a worker subagent executing a specific task. Do NOT spawn sub-agents; execute directly. Keep your report under 500 words. Use this structure:
|
|
332
|
+
- Scope: What was requested
|
|
333
|
+
- Result: What was done
|
|
334
|
+
- Key files: Relevant file paths (absolute)
|
|
335
|
+
- Files changed: List of modifications
|
|
336
|
+
- Issues: Any problems encountered`;
|
|
337
|
+
}
|
|
338
|
+
function assembleSubagentPrompt(options) {
|
|
339
|
+
const parts = [options.agentBody];
|
|
340
|
+
if (options.claudeMd) {
|
|
341
|
+
parts.push(options.claudeMd);
|
|
342
|
+
}
|
|
343
|
+
if (options.agentsMd) {
|
|
344
|
+
parts.push(options.agentsMd);
|
|
345
|
+
}
|
|
346
|
+
const suffix = options.isForkWorker ? getForkWorkerSuffix() : getSubagentSuffix();
|
|
347
|
+
parts.push(suffix);
|
|
348
|
+
return parts.join("\n\n");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/assembly/create-subagent-session.ts
|
|
352
|
+
var MODEL_SHORTCUTS = {
|
|
353
|
+
sonnet: "claude-sonnet-4-6",
|
|
354
|
+
haiku: "claude-haiku-4-5",
|
|
355
|
+
opus: "claude-opus-4-6"
|
|
356
|
+
};
|
|
357
|
+
function resolveModelId(shortName, _parentModel) {
|
|
358
|
+
return MODEL_SHORTCUTS[shortName] ?? shortName;
|
|
359
|
+
}
|
|
360
|
+
function filterTools(parentTools, agentDefinition) {
|
|
361
|
+
let tools = [...parentTools];
|
|
362
|
+
if (agentDefinition.disallowedTools) {
|
|
363
|
+
const denySet = new Set(agentDefinition.disallowedTools);
|
|
364
|
+
tools = tools.filter((t) => !denySet.has(t.getName()));
|
|
365
|
+
}
|
|
366
|
+
if (agentDefinition.tools) {
|
|
367
|
+
const allowSet = new Set(agentDefinition.tools);
|
|
368
|
+
tools = tools.filter((t) => allowSet.has(t.getName()));
|
|
369
|
+
}
|
|
370
|
+
tools = tools.filter((t) => t.getName() !== "Agent");
|
|
371
|
+
return tools;
|
|
372
|
+
}
|
|
373
|
+
function createSubagentSession(options) {
|
|
374
|
+
const { agentDefinition, parentConfig, parentContext, parentTools, terminal } = options;
|
|
375
|
+
const tools = filterTools(parentTools, agentDefinition);
|
|
376
|
+
const model = agentDefinition.model ? resolveModelId(agentDefinition.model, parentConfig.provider.model) : parentConfig.provider.model;
|
|
377
|
+
const systemMessage = assembleSubagentPrompt({
|
|
378
|
+
agentBody: agentDefinition.systemPrompt,
|
|
379
|
+
claudeMd: parentContext.claudeMd,
|
|
380
|
+
agentsMd: parentContext.agentsMd,
|
|
381
|
+
isForkWorker: options.isForkWorker ?? false
|
|
382
|
+
});
|
|
383
|
+
const provider = createProvider(parentConfig);
|
|
384
|
+
return new Session({
|
|
385
|
+
tools,
|
|
386
|
+
provider,
|
|
387
|
+
systemMessage,
|
|
388
|
+
terminal,
|
|
389
|
+
model,
|
|
390
|
+
maxTurns: agentDefinition.maxTurns,
|
|
391
|
+
permissions: parentConfig.permissions,
|
|
392
|
+
permissionMode: options.permissionMode,
|
|
393
|
+
defaultTrustLevel: parentConfig.defaultTrustLevel,
|
|
394
|
+
permissionHandler: options.permissionHandler,
|
|
395
|
+
hooks: options.hooks,
|
|
396
|
+
hookTypeExecutors: options.hookTypeExecutors,
|
|
397
|
+
onTextDelta: options.onTextDelta,
|
|
398
|
+
onToolExecution: options.onToolExecution
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/tools/agent-tool.ts
|
|
403
|
+
function asZodSchema(schema) {
|
|
404
|
+
return schema;
|
|
405
|
+
}
|
|
406
|
+
var AgentSchema = z.object({
|
|
407
|
+
prompt: z.string().describe("The task for the subagent to perform"),
|
|
408
|
+
subagent_type: z.string().optional().describe('Agent type: "general-purpose", "Explore", "Plan", or a custom agent name'),
|
|
409
|
+
model: z.string().optional().describe("Optional model override")
|
|
410
|
+
});
|
|
411
|
+
var sessionDepsStore = /* @__PURE__ */ new WeakMap();
|
|
412
|
+
function storeAgentToolDeps(key, deps) {
|
|
413
|
+
sessionDepsStore.set(key, deps);
|
|
414
|
+
}
|
|
415
|
+
function retrieveAgentToolDeps(key) {
|
|
416
|
+
return sessionDepsStore.get(key);
|
|
417
|
+
}
|
|
418
|
+
function resolveAgentDefinition(agentType, customRegistry) {
|
|
419
|
+
const builtIn = getBuiltInAgent(agentType);
|
|
420
|
+
if (builtIn) return builtIn;
|
|
421
|
+
if (customRegistry) return customRegistry(agentType);
|
|
422
|
+
return void 0;
|
|
423
|
+
}
|
|
424
|
+
function generateAgentId() {
|
|
425
|
+
return `agent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
426
|
+
}
|
|
427
|
+
function createAgentTool(deps) {
|
|
428
|
+
async function runAgent(args) {
|
|
429
|
+
const agentType = args.subagent_type ?? "general-purpose";
|
|
430
|
+
const agentDef = resolveAgentDefinition(agentType, deps.customAgentRegistry);
|
|
431
|
+
if (!agentDef) {
|
|
432
|
+
return JSON.stringify({
|
|
433
|
+
success: false,
|
|
434
|
+
output: "",
|
|
435
|
+
error: `Unknown agent type: ${agentType}`
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const effectiveDef = args.model ? { ...agentDef, model: args.model } : agentDef;
|
|
439
|
+
const session = createSubagentSession({
|
|
440
|
+
agentDefinition: effectiveDef,
|
|
441
|
+
parentConfig: deps.config,
|
|
442
|
+
parentContext: deps.context,
|
|
443
|
+
parentTools: deps.tools,
|
|
444
|
+
terminal: deps.terminal,
|
|
445
|
+
permissionMode: deps.permissionMode,
|
|
446
|
+
permissionHandler: deps.permissionHandler,
|
|
447
|
+
hooks: deps.hooks,
|
|
448
|
+
hookTypeExecutors: deps.hookTypeExecutors,
|
|
449
|
+
onTextDelta: deps.onTextDelta,
|
|
450
|
+
onToolExecution: deps.onToolExecution
|
|
451
|
+
});
|
|
452
|
+
const agentId = generateAgentId();
|
|
453
|
+
try {
|
|
454
|
+
const response = await session.run(args.prompt);
|
|
455
|
+
return JSON.stringify({
|
|
456
|
+
success: true,
|
|
457
|
+
output: response,
|
|
458
|
+
agentId
|
|
459
|
+
});
|
|
460
|
+
} catch (err) {
|
|
461
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
462
|
+
return JSON.stringify({
|
|
463
|
+
success: false,
|
|
464
|
+
output: "",
|
|
465
|
+
error: `Sub-agent error: ${message}`,
|
|
466
|
+
agentId
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return createZodFunctionTool(
|
|
471
|
+
"Agent",
|
|
472
|
+
"Launch a subagent to handle a task in an isolated context. The subagent gets its own context window and returns a result when done. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.",
|
|
473
|
+
asZodSchema(AgentSchema),
|
|
474
|
+
async (params) => {
|
|
475
|
+
return runAgent(params);
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/agents/agent-definition-loader.ts
|
|
481
|
+
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
482
|
+
import { join, basename } from "path";
|
|
483
|
+
import { homedir } from "os";
|
|
484
|
+
var LIST_KEYS = /* @__PURE__ */ new Set(["tools", "disallowedTools"]);
|
|
485
|
+
var NUMBER_KEYS = /* @__PURE__ */ new Set(["maxTurns"]);
|
|
486
|
+
function parseFrontmatter(content) {
|
|
487
|
+
const lines = content.split("\n");
|
|
488
|
+
if (lines[0]?.trim() !== "---") {
|
|
489
|
+
return { frontmatter: null, body: content };
|
|
490
|
+
}
|
|
491
|
+
let endIndex = -1;
|
|
492
|
+
for (let i = 1; i < lines.length; i++) {
|
|
493
|
+
if (lines[i]?.trim() === "---") {
|
|
494
|
+
endIndex = i;
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (endIndex === -1) {
|
|
499
|
+
return { frontmatter: null, body: content };
|
|
500
|
+
}
|
|
501
|
+
const result = {};
|
|
502
|
+
for (let i = 1; i < endIndex; i++) {
|
|
503
|
+
const line = lines[i];
|
|
504
|
+
const match = line.match(/^([a-zA-Z][a-zA-Z0-9]*(?:[A-Z][a-z]*)*):\s*(.+)/);
|
|
505
|
+
if (!match) continue;
|
|
506
|
+
const key = match[1];
|
|
507
|
+
const rawValue = match[2].trim();
|
|
508
|
+
if (LIST_KEYS.has(key)) {
|
|
509
|
+
result[key] = rawValue.split(",").map((s) => s.trim());
|
|
510
|
+
} else if (NUMBER_KEYS.has(key)) {
|
|
511
|
+
result[key] = parseInt(rawValue, 10);
|
|
512
|
+
} else {
|
|
513
|
+
result[key] = rawValue;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const body = lines.slice(endIndex + 1).join("\n").trim();
|
|
517
|
+
return {
|
|
518
|
+
frontmatter: Object.keys(result).length > 0 ? result : null,
|
|
519
|
+
body
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function scanAgentsDir(dir) {
|
|
523
|
+
if (!existsSync(dir)) return [];
|
|
524
|
+
const agents = [];
|
|
525
|
+
let entries;
|
|
526
|
+
try {
|
|
527
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
528
|
+
} catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
for (const entry of entries) {
|
|
532
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
533
|
+
const filePath = join(dir, entry.name);
|
|
534
|
+
const content = readFileSync(filePath, "utf-8");
|
|
535
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
536
|
+
const fallbackName = basename(entry.name, ".md");
|
|
537
|
+
const agent = {
|
|
538
|
+
name: frontmatter?.name ?? fallbackName,
|
|
539
|
+
description: frontmatter?.description ?? "",
|
|
540
|
+
systemPrompt: body
|
|
541
|
+
};
|
|
542
|
+
if (frontmatter?.model !== void 0) agent.model = frontmatter.model;
|
|
543
|
+
if (frontmatter?.maxTurns !== void 0) agent.maxTurns = frontmatter.maxTurns;
|
|
544
|
+
if (frontmatter?.tools !== void 0) agent.tools = frontmatter.tools;
|
|
545
|
+
if (frontmatter?.disallowedTools !== void 0)
|
|
546
|
+
agent.disallowedTools = frontmatter.disallowedTools;
|
|
547
|
+
agents.push(agent);
|
|
548
|
+
}
|
|
549
|
+
return agents;
|
|
550
|
+
}
|
|
551
|
+
var AgentDefinitionLoader = class {
|
|
552
|
+
cwd;
|
|
553
|
+
home;
|
|
554
|
+
constructor(cwd, home) {
|
|
555
|
+
this.cwd = cwd;
|
|
556
|
+
this.home = home ?? homedir();
|
|
557
|
+
}
|
|
558
|
+
/** Load all agent definitions, merged with built-in agents. Custom overrides built-in on name collision. */
|
|
559
|
+
loadAll() {
|
|
560
|
+
const sources = [
|
|
561
|
+
scanAgentsDir(join(this.cwd, ".claude", "agents")),
|
|
562
|
+
scanAgentsDir(join(this.home, ".robota", "agents"))
|
|
563
|
+
];
|
|
564
|
+
const seen = /* @__PURE__ */ new Set();
|
|
565
|
+
const customAgents = [];
|
|
566
|
+
for (const agents of sources) {
|
|
567
|
+
for (const agent of agents) {
|
|
568
|
+
if (!seen.has(agent.name)) {
|
|
569
|
+
seen.add(agent.name);
|
|
570
|
+
customAgents.push(agent);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const result = [...customAgents];
|
|
575
|
+
for (const builtIn of BUILT_IN_AGENTS) {
|
|
576
|
+
if (!seen.has(builtIn.name)) {
|
|
577
|
+
result.push(builtIn);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
/** Get a specific agent by name (custom or built-in). */
|
|
583
|
+
getAgent(name) {
|
|
584
|
+
return this.loadAll().find((agent) => agent.name === name);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
|
|
120
588
|
// src/assembly/create-session.ts
|
|
121
589
|
function createSession(options) {
|
|
122
590
|
const provider = options.provider ?? createProvider(options.config);
|
|
123
591
|
const defaultTools = createDefaultTools();
|
|
124
592
|
const tools = [...defaultTools, ...options.additionalTools ?? []];
|
|
593
|
+
const agentLoader = new AgentDefinitionLoader(process.cwd());
|
|
594
|
+
const hookTypeExecutors = [];
|
|
595
|
+
if (options.providerFactory) {
|
|
596
|
+
hookTypeExecutors.push(
|
|
597
|
+
new PromptExecutor({
|
|
598
|
+
providerFactory: options.providerFactory,
|
|
599
|
+
defaultModel: options.config.provider.model
|
|
600
|
+
})
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (options.sessionFactory) {
|
|
604
|
+
hookTypeExecutors.push(
|
|
605
|
+
new AgentExecutor({
|
|
606
|
+
sessionFactory: options.sessionFactory
|
|
607
|
+
})
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
if (options.additionalHookExecutors) {
|
|
611
|
+
hookTypeExecutors.push(...options.additionalHookExecutors);
|
|
612
|
+
}
|
|
613
|
+
const agentToolDeps = {
|
|
614
|
+
config: options.config,
|
|
615
|
+
context: options.context,
|
|
616
|
+
tools,
|
|
617
|
+
terminal: options.terminal,
|
|
618
|
+
permissionMode: options.permissionMode,
|
|
619
|
+
permissionHandler: options.permissionHandler,
|
|
620
|
+
hooks: options.config.hooks,
|
|
621
|
+
hookTypeExecutors: hookTypeExecutors.length > 0 ? hookTypeExecutors : void 0,
|
|
622
|
+
onTextDelta: options.onTextDelta,
|
|
623
|
+
onToolExecution: options.onToolExecution,
|
|
624
|
+
customAgentRegistry: (name) => agentLoader.getAgent(name)
|
|
625
|
+
};
|
|
626
|
+
tools.push(createAgentTool(agentToolDeps));
|
|
125
627
|
const buildPrompt = options.systemPromptBuilder ?? buildSystemPrompt;
|
|
126
628
|
const systemMessage = buildPrompt({
|
|
127
629
|
agentsMd: options.context.agentsMd,
|
|
128
630
|
claudeMd: options.context.claudeMd,
|
|
129
631
|
toolDescriptions: options.toolDescriptions ?? DEFAULT_TOOL_DESCRIPTIONS,
|
|
130
632
|
trustLevel: options.config.defaultTrustLevel,
|
|
131
|
-
projectInfo: options.projectInfo ?? { type: "unknown", language: "unknown" }
|
|
633
|
+
projectInfo: options.projectInfo ?? { type: "unknown", language: "unknown" },
|
|
634
|
+
cwd: process.cwd(),
|
|
635
|
+
language: options.config.language
|
|
132
636
|
});
|
|
133
|
-
|
|
637
|
+
const defaultAllow = [
|
|
638
|
+
"Read(.agents/**)",
|
|
639
|
+
"Read(.claude/**)",
|
|
640
|
+
"Read(.robota/**)",
|
|
641
|
+
"Glob(.agents/**)",
|
|
642
|
+
"Glob(.claude/**)",
|
|
643
|
+
"Glob(.robota/**)"
|
|
644
|
+
];
|
|
645
|
+
const mergedPermissions = {
|
|
646
|
+
allow: [...defaultAllow, ...options.config.permissions.allow ?? []],
|
|
647
|
+
deny: options.config.permissions.deny ?? []
|
|
648
|
+
};
|
|
649
|
+
const session = new Session2({
|
|
134
650
|
tools,
|
|
135
651
|
provider,
|
|
136
652
|
systemMessage,
|
|
137
653
|
terminal: options.terminal,
|
|
138
|
-
permissions:
|
|
654
|
+
permissions: mergedPermissions,
|
|
139
655
|
hooks: options.config.hooks,
|
|
140
656
|
permissionMode: options.permissionMode,
|
|
141
657
|
defaultTrustLevel: options.config.defaultTrustLevel,
|
|
@@ -144,57 +660,119 @@ function createSession(options) {
|
|
|
144
660
|
sessionStore: options.sessionStore,
|
|
145
661
|
permissionHandler: options.permissionHandler,
|
|
146
662
|
onTextDelta: options.onTextDelta,
|
|
663
|
+
onToolExecution: options.onToolExecution,
|
|
147
664
|
promptForApproval: options.promptForApproval,
|
|
148
665
|
onCompact: options.onCompact,
|
|
149
666
|
compactInstructions: options.compactInstructions ?? options.context.compactInstructions,
|
|
150
|
-
sessionLogger: options.sessionLogger
|
|
667
|
+
sessionLogger: options.sessionLogger,
|
|
668
|
+
hookTypeExecutors: hookTypeExecutors.length > 0 ? hookTypeExecutors : void 0
|
|
151
669
|
});
|
|
670
|
+
storeAgentToolDeps(session, agentToolDeps);
|
|
671
|
+
return session;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/assembly/subagent-logger.ts
|
|
675
|
+
import { mkdirSync } from "fs";
|
|
676
|
+
import { join as join2 } from "path";
|
|
677
|
+
import { FileSessionLogger } from "@robota-sdk/agent-sessions";
|
|
678
|
+
function createSubagentLogger(parentSessionId, _agentId, baseLogsDir) {
|
|
679
|
+
const subagentDir = join2(baseLogsDir, parentSessionId, "subagents");
|
|
680
|
+
mkdirSync(subagentDir, { recursive: true });
|
|
681
|
+
return new FileSessionLogger(subagentDir);
|
|
682
|
+
}
|
|
683
|
+
function resolveSubagentLogDir(parentSessionId, baseLogsDir) {
|
|
684
|
+
return join2(baseLogsDir, parentSessionId, "subagents");
|
|
152
685
|
}
|
|
153
686
|
|
|
154
687
|
// src/index.ts
|
|
155
|
-
import { Session as
|
|
156
|
-
import { FileSessionLogger, SilentSessionLogger } from "@robota-sdk/agent-sessions";
|
|
688
|
+
import { Session as Session3 } from "@robota-sdk/agent-sessions";
|
|
689
|
+
import { FileSessionLogger as FileSessionLogger3, SilentSessionLogger } from "@robota-sdk/agent-sessions";
|
|
157
690
|
import { SessionStore } from "@robota-sdk/agent-sessions";
|
|
158
691
|
|
|
159
692
|
// src/config/config-loader.ts
|
|
160
|
-
import { readFileSync, existsSync } from "fs";
|
|
161
|
-
import { join } from "path";
|
|
693
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
694
|
+
import { join as join3 } from "path";
|
|
162
695
|
|
|
163
696
|
// src/config/config-types.ts
|
|
164
|
-
import { z } from "zod";
|
|
165
|
-
var ProviderSchema =
|
|
166
|
-
name:
|
|
167
|
-
model:
|
|
168
|
-
apiKey:
|
|
697
|
+
import { z as z2 } from "zod";
|
|
698
|
+
var ProviderSchema = z2.object({
|
|
699
|
+
name: z2.string().optional(),
|
|
700
|
+
model: z2.string().optional(),
|
|
701
|
+
apiKey: z2.string().optional()
|
|
169
702
|
});
|
|
170
|
-
var PermissionsSchema =
|
|
703
|
+
var PermissionsSchema = z2.object({
|
|
171
704
|
/** Patterns that are always approved without prompting */
|
|
172
|
-
allow:
|
|
705
|
+
allow: z2.array(z2.string()).optional(),
|
|
173
706
|
/** Patterns that are always denied */
|
|
174
|
-
deny:
|
|
707
|
+
deny: z2.array(z2.string()).optional()
|
|
175
708
|
});
|
|
176
|
-
var EnvSchema =
|
|
177
|
-
var
|
|
178
|
-
type:
|
|
179
|
-
command:
|
|
709
|
+
var EnvSchema = z2.record(z2.string()).optional();
|
|
710
|
+
var CommandHookDefinitionSchema = z2.object({
|
|
711
|
+
type: z2.literal("command"),
|
|
712
|
+
command: z2.string(),
|
|
713
|
+
timeout: z2.number().optional()
|
|
180
714
|
});
|
|
181
|
-
var
|
|
182
|
-
|
|
183
|
-
|
|
715
|
+
var HttpHookDefinitionSchema = z2.object({
|
|
716
|
+
type: z2.literal("http"),
|
|
717
|
+
url: z2.string(),
|
|
718
|
+
headers: z2.record(z2.string()).optional(),
|
|
719
|
+
timeout: z2.number().optional()
|
|
184
720
|
});
|
|
185
|
-
var
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
721
|
+
var PromptHookDefinitionSchema = z2.object({
|
|
722
|
+
type: z2.literal("prompt"),
|
|
723
|
+
prompt: z2.string(),
|
|
724
|
+
model: z2.string().optional()
|
|
725
|
+
});
|
|
726
|
+
var AgentHookDefinitionSchema = z2.object({
|
|
727
|
+
type: z2.literal("agent"),
|
|
728
|
+
agent: z2.string(),
|
|
729
|
+
maxTurns: z2.number().optional(),
|
|
730
|
+
timeout: z2.number().optional()
|
|
731
|
+
});
|
|
732
|
+
var HookDefinitionSchema = z2.discriminatedUnion("type", [
|
|
733
|
+
CommandHookDefinitionSchema,
|
|
734
|
+
HttpHookDefinitionSchema,
|
|
735
|
+
PromptHookDefinitionSchema,
|
|
736
|
+
AgentHookDefinitionSchema
|
|
737
|
+
]);
|
|
738
|
+
var HookGroupSchema = z2.object({
|
|
739
|
+
matcher: z2.string(),
|
|
740
|
+
hooks: z2.array(HookDefinitionSchema)
|
|
741
|
+
});
|
|
742
|
+
var HooksSchema = z2.object({
|
|
743
|
+
PreToolUse: z2.array(HookGroupSchema).optional(),
|
|
744
|
+
PostToolUse: z2.array(HookGroupSchema).optional(),
|
|
745
|
+
SessionStart: z2.array(HookGroupSchema).optional(),
|
|
746
|
+
Stop: z2.array(HookGroupSchema).optional(),
|
|
747
|
+
PreCompact: z2.array(HookGroupSchema).optional(),
|
|
748
|
+
PostCompact: z2.array(HookGroupSchema).optional(),
|
|
749
|
+
UserPromptSubmit: z2.array(HookGroupSchema).optional(),
|
|
750
|
+
Notification: z2.array(HookGroupSchema).optional()
|
|
190
751
|
}).optional();
|
|
191
|
-
var
|
|
752
|
+
var EnabledPluginsSchema = z2.record(z2.boolean()).optional();
|
|
753
|
+
var MarketplaceSourceSchema = z2.object({
|
|
754
|
+
source: z2.object({
|
|
755
|
+
type: z2.enum(["github", "git", "local", "url"]),
|
|
756
|
+
repo: z2.string().optional(),
|
|
757
|
+
url: z2.string().optional(),
|
|
758
|
+
path: z2.string().optional(),
|
|
759
|
+
ref: z2.string().optional()
|
|
760
|
+
})
|
|
761
|
+
});
|
|
762
|
+
var ExtraKnownMarketplacesSchema = z2.record(MarketplaceSourceSchema).optional();
|
|
763
|
+
var SettingsSchema = z2.object({
|
|
192
764
|
/** Trust level used when no --permission-mode flag is given */
|
|
193
|
-
defaultTrustLevel:
|
|
765
|
+
defaultTrustLevel: z2.enum(["safe", "moderate", "full"]).optional(),
|
|
766
|
+
/** Response language (e.g., "ko", "en", "ja"). Injected into system prompt. */
|
|
767
|
+
language: z2.string().optional(),
|
|
194
768
|
provider: ProviderSchema.optional(),
|
|
195
769
|
permissions: PermissionsSchema.optional(),
|
|
196
770
|
env: EnvSchema,
|
|
197
|
-
hooks: HooksSchema
|
|
771
|
+
hooks: HooksSchema,
|
|
772
|
+
/** Plugin enablement map: plugin name -> enabled/disabled */
|
|
773
|
+
enabledPlugins: EnabledPluginsSchema,
|
|
774
|
+
/** Extra marketplace URLs for BundlePlugin discovery */
|
|
775
|
+
extraKnownMarketplaces: ExtraKnownMarketplacesSchema
|
|
198
776
|
});
|
|
199
777
|
|
|
200
778
|
// src/config/config-loader.ts
|
|
@@ -215,11 +793,18 @@ var DEFAULTS = {
|
|
|
215
793
|
env: {}
|
|
216
794
|
};
|
|
217
795
|
function readJsonFile(filePath) {
|
|
218
|
-
if (!
|
|
796
|
+
if (!existsSync2(filePath)) {
|
|
797
|
+
return void 0;
|
|
798
|
+
}
|
|
799
|
+
const raw = readFileSync2(filePath, "utf-8").trim();
|
|
800
|
+
if (raw.length === 0) {
|
|
801
|
+
return void 0;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
return JSON.parse(raw);
|
|
805
|
+
} catch {
|
|
219
806
|
return void 0;
|
|
220
807
|
}
|
|
221
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
222
|
-
return JSON.parse(raw);
|
|
223
808
|
}
|
|
224
809
|
function resolveEnvRef(value) {
|
|
225
810
|
const ENV_PREFIX = "$ENV:";
|
|
@@ -254,13 +839,16 @@ function mergeSettings(layers) {
|
|
|
254
839
|
env: {
|
|
255
840
|
...merged.env ?? {},
|
|
256
841
|
...layer.env ?? {}
|
|
257
|
-
}
|
|
842
|
+
},
|
|
843
|
+
enabledPlugins: merged.enabledPlugins !== void 0 || layer.enabledPlugins !== void 0 ? { ...merged.enabledPlugins ?? {}, ...layer.enabledPlugins ?? {} } : void 0,
|
|
844
|
+
extraKnownMarketplaces: layer.extraKnownMarketplaces ?? merged.extraKnownMarketplaces
|
|
258
845
|
};
|
|
259
846
|
}, {});
|
|
260
847
|
}
|
|
261
848
|
function toResolvedConfig(merged) {
|
|
262
849
|
return {
|
|
263
850
|
defaultTrustLevel: merged.defaultTrustLevel ?? DEFAULTS.defaultTrustLevel,
|
|
851
|
+
language: merged.language,
|
|
264
852
|
provider: {
|
|
265
853
|
name: merged.provider?.name ?? DEFAULTS.provider.name,
|
|
266
854
|
model: merged.provider?.model ?? DEFAULTS.provider.model,
|
|
@@ -271,25 +859,39 @@ function toResolvedConfig(merged) {
|
|
|
271
859
|
deny: merged.permissions?.deny ?? DEFAULTS.permissions.deny
|
|
272
860
|
},
|
|
273
861
|
env: merged.env ?? DEFAULTS.env,
|
|
274
|
-
hooks: merged.hooks ?? void 0
|
|
862
|
+
hooks: merged.hooks ?? void 0,
|
|
863
|
+
enabledPlugins: merged.enabledPlugins ?? void 0,
|
|
864
|
+
extraKnownMarketplaces: merged.extraKnownMarketplaces ?? void 0
|
|
275
865
|
};
|
|
276
866
|
}
|
|
867
|
+
function getSettingsPaths(cwd) {
|
|
868
|
+
const home = getHomeDir();
|
|
869
|
+
return [
|
|
870
|
+
join3(home, ".robota", "settings.json"),
|
|
871
|
+
// 1. user (lowest)
|
|
872
|
+
join3(cwd, ".robota", "settings.json"),
|
|
873
|
+
// 2. project
|
|
874
|
+
join3(cwd, ".robota", "settings.local.json"),
|
|
875
|
+
// 3. project-local
|
|
876
|
+
join3(cwd, ".claude", "settings.json"),
|
|
877
|
+
// 4. project, Claude Code compat
|
|
878
|
+
join3(cwd, ".claude", "settings.local.json")
|
|
879
|
+
// 5. project-local (highest)
|
|
880
|
+
];
|
|
881
|
+
}
|
|
277
882
|
async function loadConfig(cwd) {
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const parsedLayers =
|
|
883
|
+
const allPaths = getSettingsPaths(cwd);
|
|
884
|
+
const rawEntries = [];
|
|
885
|
+
for (const filePath of allPaths) {
|
|
886
|
+
const raw = readJsonFile(filePath);
|
|
887
|
+
if (raw !== void 0) {
|
|
888
|
+
rawEntries.push({ raw, path: filePath });
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const parsedLayers = rawEntries.map(({ raw, path }) => {
|
|
287
892
|
const result = SettingsSchema.safeParse(raw);
|
|
288
893
|
if (!result.success) {
|
|
289
|
-
|
|
290
|
-
(_, i) => rawLayers[i] !== void 0
|
|
291
|
-
);
|
|
292
|
-
throw new Error(`Invalid settings in ${paths[index] ?? "unknown"}: ${result.error.message}`);
|
|
894
|
+
throw new Error(`Invalid settings in ${path}: ${result.error.message}`);
|
|
293
895
|
}
|
|
294
896
|
return resolveEnvRefs(result.data);
|
|
295
897
|
});
|
|
@@ -298,8 +900,8 @@ async function loadConfig(cwd) {
|
|
|
298
900
|
}
|
|
299
901
|
|
|
300
902
|
// src/context/context-loader.ts
|
|
301
|
-
import { existsSync as
|
|
302
|
-
import { join as
|
|
903
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
904
|
+
import { join as join4, dirname, resolve } from "path";
|
|
303
905
|
var AGENTS_FILENAME = "AGENTS.md";
|
|
304
906
|
var CLAUDE_FILENAME = "CLAUDE.md";
|
|
305
907
|
function collectFilesWalkingUp(startDir, filename) {
|
|
@@ -307,8 +909,8 @@ function collectFilesWalkingUp(startDir, filename) {
|
|
|
307
909
|
let current = resolve(startDir);
|
|
308
910
|
let atRoot = false;
|
|
309
911
|
while (!atRoot) {
|
|
310
|
-
const candidate =
|
|
311
|
-
if (
|
|
912
|
+
const candidate = join4(current, filename);
|
|
913
|
+
if (existsSync3(candidate)) {
|
|
312
914
|
found.push(candidate);
|
|
313
915
|
}
|
|
314
916
|
const parent = dirname(current);
|
|
@@ -346,47 +948,47 @@ function extractCompactInstructions(content) {
|
|
|
346
948
|
async function loadContext(cwd) {
|
|
347
949
|
const agentsPaths = collectFilesWalkingUp(cwd, AGENTS_FILENAME);
|
|
348
950
|
const claudePaths = collectFilesWalkingUp(cwd, CLAUDE_FILENAME);
|
|
349
|
-
const agentsMd = agentsPaths.map((p) =>
|
|
350
|
-
const claudeMd = claudePaths.map((p) =>
|
|
951
|
+
const agentsMd = agentsPaths.map((p) => readFileSync3(p, "utf-8")).join("\n\n");
|
|
952
|
+
const claudeMd = claudePaths.map((p) => readFileSync3(p, "utf-8")).join("\n\n");
|
|
351
953
|
const compactInstructions = extractCompactInstructions(claudeMd);
|
|
352
954
|
return { agentsMd, claudeMd, compactInstructions };
|
|
353
955
|
}
|
|
354
956
|
|
|
355
957
|
// src/context/project-detector.ts
|
|
356
|
-
import { existsSync as
|
|
357
|
-
import { join as
|
|
958
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
959
|
+
import { join as join5 } from "path";
|
|
358
960
|
function tryReadJson(filePath) {
|
|
359
|
-
if (!
|
|
961
|
+
if (!existsSync4(filePath)) return void 0;
|
|
360
962
|
try {
|
|
361
|
-
return JSON.parse(
|
|
963
|
+
return JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
362
964
|
} catch {
|
|
363
965
|
return void 0;
|
|
364
966
|
}
|
|
365
967
|
}
|
|
366
968
|
function detectPackageManager(cwd) {
|
|
367
|
-
if (
|
|
969
|
+
if (existsSync4(join5(cwd, "pnpm-workspace.yaml")) || existsSync4(join5(cwd, "pnpm-lock.yaml"))) {
|
|
368
970
|
return "pnpm";
|
|
369
971
|
}
|
|
370
|
-
if (
|
|
972
|
+
if (existsSync4(join5(cwd, "yarn.lock"))) {
|
|
371
973
|
return "yarn";
|
|
372
974
|
}
|
|
373
|
-
if (
|
|
975
|
+
if (existsSync4(join5(cwd, "bun.lockb"))) {
|
|
374
976
|
return "bun";
|
|
375
977
|
}
|
|
376
|
-
if (
|
|
978
|
+
if (existsSync4(join5(cwd, "package-lock.json"))) {
|
|
377
979
|
return "npm";
|
|
378
980
|
}
|
|
379
981
|
return void 0;
|
|
380
982
|
}
|
|
381
983
|
async function detectProject(cwd) {
|
|
382
|
-
const pkgJsonPath =
|
|
383
|
-
const tsconfigPath =
|
|
384
|
-
const pyprojectPath =
|
|
385
|
-
const cargoPath =
|
|
386
|
-
const goModPath =
|
|
387
|
-
if (
|
|
984
|
+
const pkgJsonPath = join5(cwd, "package.json");
|
|
985
|
+
const tsconfigPath = join5(cwd, "tsconfig.json");
|
|
986
|
+
const pyprojectPath = join5(cwd, "pyproject.toml");
|
|
987
|
+
const cargoPath = join5(cwd, "Cargo.toml");
|
|
988
|
+
const goModPath = join5(cwd, "go.mod");
|
|
989
|
+
if (existsSync4(pkgJsonPath)) {
|
|
388
990
|
const pkgJson = tryReadJson(pkgJsonPath);
|
|
389
|
-
const language =
|
|
991
|
+
const language = existsSync4(tsconfigPath) ? "typescript" : "javascript";
|
|
390
992
|
const packageManager = detectPackageManager(cwd);
|
|
391
993
|
return {
|
|
392
994
|
type: "node",
|
|
@@ -395,19 +997,19 @@ async function detectProject(cwd) {
|
|
|
395
997
|
language
|
|
396
998
|
};
|
|
397
999
|
}
|
|
398
|
-
if (
|
|
1000
|
+
if (existsSync4(pyprojectPath) || existsSync4(join5(cwd, "setup.py"))) {
|
|
399
1001
|
return {
|
|
400
1002
|
type: "python",
|
|
401
1003
|
language: "python"
|
|
402
1004
|
};
|
|
403
1005
|
}
|
|
404
|
-
if (
|
|
1006
|
+
if (existsSync4(cargoPath)) {
|
|
405
1007
|
return {
|
|
406
1008
|
type: "rust",
|
|
407
1009
|
language: "rust"
|
|
408
1010
|
};
|
|
409
1011
|
}
|
|
410
|
-
if (
|
|
1012
|
+
if (existsSync4(goModPath)) {
|
|
411
1013
|
return {
|
|
412
1014
|
type: "go",
|
|
413
1015
|
language: "go"
|
|
@@ -484,96 +1086,1528 @@ import { evaluatePermission } from "@robota-sdk/agent-core";
|
|
|
484
1086
|
import { runHooks } from "@robota-sdk/agent-core";
|
|
485
1087
|
|
|
486
1088
|
// src/paths.ts
|
|
487
|
-
import { join as
|
|
488
|
-
import { homedir } from "os";
|
|
1089
|
+
import { join as join6 } from "path";
|
|
1090
|
+
import { homedir as homedir2 } from "os";
|
|
489
1091
|
function projectPaths(cwd) {
|
|
490
|
-
const base =
|
|
1092
|
+
const base = join6(cwd, ".robota");
|
|
491
1093
|
return {
|
|
492
|
-
settings:
|
|
493
|
-
settingsLocal:
|
|
494
|
-
logs:
|
|
495
|
-
sessions:
|
|
1094
|
+
settings: join6(base, "settings.json"),
|
|
1095
|
+
settingsLocal: join6(base, "settings.local.json"),
|
|
1096
|
+
logs: join6(base, "logs"),
|
|
1097
|
+
sessions: join6(base, "sessions")
|
|
496
1098
|
};
|
|
497
1099
|
}
|
|
498
1100
|
function userPaths() {
|
|
499
|
-
const base =
|
|
1101
|
+
const base = join6(homedir2(), ".robota");
|
|
500
1102
|
return {
|
|
501
|
-
settings:
|
|
502
|
-
sessions:
|
|
1103
|
+
settings: join6(base, "settings.json"),
|
|
1104
|
+
sessions: join6(base, "sessions")
|
|
503
1105
|
};
|
|
504
1106
|
}
|
|
505
1107
|
|
|
506
|
-
// src/
|
|
507
|
-
import {
|
|
508
|
-
import {
|
|
509
|
-
|
|
510
|
-
|
|
1108
|
+
// src/plugins/plugin-settings-store.ts
|
|
1109
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1110
|
+
import { dirname as dirname2 } from "path";
|
|
1111
|
+
var PluginSettingsStore = class {
|
|
1112
|
+
settingsPath;
|
|
1113
|
+
constructor(settingsPath) {
|
|
1114
|
+
this.settingsPath = settingsPath;
|
|
1115
|
+
}
|
|
1116
|
+
/** Read the full settings file from disk. */
|
|
1117
|
+
readAll() {
|
|
1118
|
+
if (!existsSync5(this.settingsPath)) {
|
|
1119
|
+
return {};
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const raw = readFileSync5(this.settingsPath, "utf-8");
|
|
1123
|
+
const data = JSON.parse(raw);
|
|
1124
|
+
if (typeof data === "object" && data !== null) {
|
|
1125
|
+
return data;
|
|
1126
|
+
}
|
|
1127
|
+
return {};
|
|
1128
|
+
} catch {
|
|
1129
|
+
return {};
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/** Write the full settings file to disk. */
|
|
1133
|
+
writeAll(settings) {
|
|
1134
|
+
const dir = dirname2(this.settingsPath);
|
|
1135
|
+
if (!existsSync5(dir)) {
|
|
1136
|
+
mkdirSync2(dir, { recursive: true });
|
|
1137
|
+
}
|
|
1138
|
+
writeFileSync(this.settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1139
|
+
}
|
|
1140
|
+
// --- enabledPlugins ---
|
|
1141
|
+
/** Get the enabledPlugins map. */
|
|
1142
|
+
getEnabledPlugins() {
|
|
1143
|
+
const settings = this.readAll();
|
|
1144
|
+
const ep = settings.enabledPlugins;
|
|
1145
|
+
if (typeof ep === "object" && ep !== null) {
|
|
1146
|
+
return ep;
|
|
1147
|
+
}
|
|
1148
|
+
return {};
|
|
1149
|
+
}
|
|
1150
|
+
/** Set a single plugin's enabled state. */
|
|
1151
|
+
setPluginEnabled(pluginId, enabled) {
|
|
1152
|
+
const settings = this.readAll();
|
|
1153
|
+
const ep = this.getEnabledPluginsFrom(settings);
|
|
1154
|
+
ep[pluginId] = enabled;
|
|
1155
|
+
settings.enabledPlugins = ep;
|
|
1156
|
+
this.writeAll(settings);
|
|
1157
|
+
}
|
|
1158
|
+
/** Remove a plugin from enabledPlugins. */
|
|
1159
|
+
removePluginEntry(pluginId) {
|
|
1160
|
+
const settings = this.readAll();
|
|
1161
|
+
const ep = this.getEnabledPluginsFrom(settings);
|
|
1162
|
+
delete ep[pluginId];
|
|
1163
|
+
settings.enabledPlugins = ep;
|
|
1164
|
+
this.writeAll(settings);
|
|
1165
|
+
}
|
|
1166
|
+
// --- extraKnownMarketplaces ---
|
|
1167
|
+
/** Get all persisted marketplace sources. */
|
|
1168
|
+
getMarketplaceSources() {
|
|
1169
|
+
const settings = this.readAll();
|
|
1170
|
+
const extra = settings.extraKnownMarketplaces;
|
|
1171
|
+
if (typeof extra === "object" && extra !== null) {
|
|
1172
|
+
return extra;
|
|
1173
|
+
}
|
|
1174
|
+
return {};
|
|
1175
|
+
}
|
|
1176
|
+
/** Add or update a marketplace source. */
|
|
1177
|
+
setMarketplaceSource(name, source) {
|
|
1178
|
+
const settings = this.readAll();
|
|
1179
|
+
const extra = this.getMarketplaceSourcesFrom(settings);
|
|
1180
|
+
extra[name] = { source };
|
|
1181
|
+
settings.extraKnownMarketplaces = extra;
|
|
1182
|
+
this.writeAll(settings);
|
|
1183
|
+
}
|
|
1184
|
+
/** Remove a marketplace source. */
|
|
1185
|
+
removeMarketplaceSource(name) {
|
|
1186
|
+
const settings = this.readAll();
|
|
1187
|
+
const extra = this.getMarketplaceSourcesFrom(settings);
|
|
1188
|
+
delete extra[name];
|
|
1189
|
+
settings.extraKnownMarketplaces = extra;
|
|
1190
|
+
this.writeAll(settings);
|
|
1191
|
+
}
|
|
1192
|
+
// --- helpers ---
|
|
1193
|
+
getEnabledPluginsFrom(settings) {
|
|
1194
|
+
const ep = settings.enabledPlugins;
|
|
1195
|
+
if (typeof ep === "object" && ep !== null) {
|
|
1196
|
+
return ep;
|
|
1197
|
+
}
|
|
1198
|
+
return {};
|
|
1199
|
+
}
|
|
1200
|
+
getMarketplaceSourcesFrom(settings) {
|
|
1201
|
+
const extra = settings.extraKnownMarketplaces;
|
|
1202
|
+
if (typeof extra === "object" && extra !== null) {
|
|
1203
|
+
return extra;
|
|
1204
|
+
}
|
|
1205
|
+
return {};
|
|
1206
|
+
}
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
// src/plugins/bundle-plugin-loader.ts
|
|
1210
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
|
|
1211
|
+
import { join as join7 } from "path";
|
|
1212
|
+
function parseSkillFrontmatter(raw) {
|
|
1213
|
+
const trimmed = raw.trimStart();
|
|
1214
|
+
if (!trimmed.startsWith("---")) {
|
|
1215
|
+
return { metadata: {}, content: raw };
|
|
1216
|
+
}
|
|
1217
|
+
const endIndex = trimmed.indexOf("---", 3);
|
|
1218
|
+
if (endIndex === -1) {
|
|
1219
|
+
return { metadata: {}, content: raw };
|
|
1220
|
+
}
|
|
1221
|
+
const frontmatterBlock = trimmed.slice(3, endIndex).trim();
|
|
1222
|
+
const content = trimmed.slice(endIndex + 3).trimStart();
|
|
1223
|
+
const metadata = {};
|
|
1224
|
+
for (const line of frontmatterBlock.split("\n")) {
|
|
1225
|
+
const colonIndex = line.indexOf(":");
|
|
1226
|
+
if (colonIndex === -1) continue;
|
|
1227
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1228
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
1229
|
+
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
|
|
1230
|
+
const inner = value.slice(1, -1);
|
|
1231
|
+
value = inner.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1232
|
+
}
|
|
1233
|
+
if (key) {
|
|
1234
|
+
metadata[key] = value;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return { metadata, content };
|
|
511
1238
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
1239
|
+
function validateManifest(data) {
|
|
1240
|
+
if (typeof data !== "object" || data === null) return null;
|
|
1241
|
+
const obj = data;
|
|
1242
|
+
if (typeof obj.name !== "string") return null;
|
|
1243
|
+
if (typeof obj.version !== "string") return null;
|
|
1244
|
+
if (typeof obj.description !== "string") return null;
|
|
1245
|
+
const features = typeof obj.features === "object" && obj.features !== null ? obj.features : {};
|
|
1246
|
+
return {
|
|
1247
|
+
name: obj.name,
|
|
1248
|
+
version: obj.version,
|
|
1249
|
+
description: obj.description,
|
|
1250
|
+
features: {
|
|
1251
|
+
commands: features.commands === true ? true : void 0,
|
|
1252
|
+
agents: features.agents === true ? true : void 0,
|
|
1253
|
+
skills: features.skills === true ? true : void 0,
|
|
1254
|
+
hooks: features.hooks === true ? true : void 0,
|
|
1255
|
+
mcp: features.mcp === true ? true : void 0
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function getSortedSubdirs(dirPath) {
|
|
1260
|
+
if (!existsSync6(dirPath)) return [];
|
|
1261
|
+
try {
|
|
1262
|
+
const entries = readdirSync2(dirPath, { withFileTypes: true });
|
|
1263
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
1264
|
+
} catch {
|
|
1265
|
+
return [];
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
var BundlePluginLoader = class {
|
|
1269
|
+
pluginsDir;
|
|
1270
|
+
enabledPlugins;
|
|
1271
|
+
constructor(pluginsDir, enabledPlugins) {
|
|
1272
|
+
this.pluginsDir = pluginsDir;
|
|
1273
|
+
this.enabledPlugins = enabledPlugins ?? {};
|
|
1274
|
+
}
|
|
1275
|
+
/** Load all discovered and enabled bundle plugins (sync). */
|
|
1276
|
+
loadPluginsSync() {
|
|
1277
|
+
return this.discoverAndLoad();
|
|
1278
|
+
}
|
|
1279
|
+
/** Load all discovered and enabled bundle plugins (async wrapper). */
|
|
1280
|
+
async loadAll() {
|
|
1281
|
+
return this.discoverAndLoad();
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Discover and load plugins from the cache directory.
|
|
1285
|
+
*
|
|
1286
|
+
* Directory structure: `<pluginsDir>/cache/<marketplace>/<plugin>/<version>/`
|
|
1287
|
+
* For each marketplace/plugin pair, the latest version (lexicographically last) is loaded.
|
|
1288
|
+
*/
|
|
1289
|
+
discoverAndLoad() {
|
|
1290
|
+
const cacheDir = join7(this.pluginsDir, "cache");
|
|
1291
|
+
if (!existsSync6(cacheDir)) {
|
|
1292
|
+
return [];
|
|
1293
|
+
}
|
|
1294
|
+
const results = [];
|
|
1295
|
+
const marketplaces = getSortedSubdirs(cacheDir);
|
|
1296
|
+
for (const marketplace of marketplaces) {
|
|
1297
|
+
const marketplaceDir = join7(cacheDir, marketplace);
|
|
1298
|
+
const plugins = getSortedSubdirs(marketplaceDir);
|
|
1299
|
+
for (const pluginName of plugins) {
|
|
1300
|
+
const pluginDir = join7(marketplaceDir, pluginName);
|
|
1301
|
+
const versions = getSortedSubdirs(pluginDir);
|
|
1302
|
+
if (versions.length === 0) continue;
|
|
1303
|
+
const latestVersion = versions[versions.length - 1];
|
|
1304
|
+
const versionDir = join7(pluginDir, latestVersion);
|
|
1305
|
+
const manifestPath = join7(versionDir, ".claude-plugin", "plugin.json");
|
|
1306
|
+
if (!existsSync6(manifestPath)) continue;
|
|
1307
|
+
const manifest = this.readManifest(manifestPath);
|
|
1308
|
+
if (!manifest) continue;
|
|
1309
|
+
const pluginId = `${manifest.name}@${marketplace}`;
|
|
1310
|
+
if (this.isDisabled(pluginId, manifest.name)) continue;
|
|
1311
|
+
const loaded = this.loadPlugin(versionDir, manifest);
|
|
1312
|
+
results.push(loaded);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return results;
|
|
1316
|
+
}
|
|
1317
|
+
/** Read and validate a plugin.json manifest. Returns null on failure. */
|
|
1318
|
+
readManifest(path) {
|
|
1319
|
+
try {
|
|
1320
|
+
const raw = readFileSync6(path, "utf-8");
|
|
1321
|
+
const data = JSON.parse(raw);
|
|
1322
|
+
return validateManifest(data);
|
|
1323
|
+
} catch {
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Check if a plugin is explicitly disabled.
|
|
1329
|
+
* Checks both `name@marketplace` and `name` keys.
|
|
1330
|
+
* Plugins not listed in enabledPlugins are enabled by default.
|
|
1331
|
+
*/
|
|
1332
|
+
isDisabled(pluginId, pluginName) {
|
|
1333
|
+
if (pluginId in this.enabledPlugins) {
|
|
1334
|
+
return this.enabledPlugins[pluginId] === false;
|
|
1335
|
+
}
|
|
1336
|
+
if (pluginName in this.enabledPlugins) {
|
|
1337
|
+
return this.enabledPlugins[pluginName] === false;
|
|
1338
|
+
}
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
/** Load a single plugin's skills, hooks, agents, and MCP config. */
|
|
1342
|
+
loadPlugin(pluginDir, manifest) {
|
|
1343
|
+
return {
|
|
1344
|
+
manifest,
|
|
1345
|
+
skills: this.loadSkills(pluginDir, manifest.name),
|
|
1346
|
+
commands: this.loadCommands(pluginDir, manifest.name),
|
|
1347
|
+
hooks: this.loadHooks(pluginDir),
|
|
1348
|
+
mcpConfig: this.loadMcpConfig(pluginDir),
|
|
1349
|
+
agents: this.loadAgents(pluginDir),
|
|
1350
|
+
pluginDir
|
|
526
1351
|
};
|
|
527
|
-
return JSON.stringify(result);
|
|
528
1352
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
1353
|
+
/** Load skills from the plugin's skills/ directory. */
|
|
1354
|
+
loadSkills(pluginDir, pluginName) {
|
|
1355
|
+
const skillsDir = join7(pluginDir, "skills");
|
|
1356
|
+
if (!existsSync6(skillsDir)) return [];
|
|
1357
|
+
const entries = readdirSync2(skillsDir, { withFileTypes: true });
|
|
1358
|
+
const skills = [];
|
|
1359
|
+
for (const entry of entries) {
|
|
1360
|
+
if (!entry.isDirectory()) continue;
|
|
1361
|
+
const skillFile = join7(skillsDir, entry.name, "SKILL.md");
|
|
1362
|
+
if (!existsSync6(skillFile)) continue;
|
|
1363
|
+
const raw = readFileSync6(skillFile, "utf-8");
|
|
1364
|
+
const { metadata, content } = parseSkillFrontmatter(raw);
|
|
1365
|
+
const description = typeof metadata.description === "string" ? metadata.description : "";
|
|
1366
|
+
const skill = {
|
|
1367
|
+
name: entry.name,
|
|
1368
|
+
description,
|
|
1369
|
+
skillContent: content,
|
|
1370
|
+
...metadata
|
|
1371
|
+
};
|
|
1372
|
+
skills.push(skill);
|
|
1373
|
+
}
|
|
1374
|
+
return skills;
|
|
1375
|
+
}
|
|
1376
|
+
/** Load commands from the plugin's commands/ directory (flat .md files). */
|
|
1377
|
+
loadCommands(pluginDir, pluginName) {
|
|
1378
|
+
const commandsDir = join7(pluginDir, "commands");
|
|
1379
|
+
if (!existsSync6(commandsDir)) return [];
|
|
1380
|
+
const entries = readdirSync2(commandsDir, { withFileTypes: true });
|
|
1381
|
+
const commands = [];
|
|
1382
|
+
for (const entry of entries) {
|
|
1383
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
1384
|
+
const raw = readFileSync6(join7(commandsDir, entry.name), "utf-8");
|
|
1385
|
+
const { metadata, content } = parseSkillFrontmatter(raw);
|
|
1386
|
+
const name = typeof metadata.name === "string" ? metadata.name : entry.name.replace(/\.md$/, "");
|
|
1387
|
+
const description = typeof metadata.description === "string" ? metadata.description : "";
|
|
1388
|
+
commands.push({
|
|
1389
|
+
...metadata,
|
|
1390
|
+
name: `${pluginName}:${name}`,
|
|
1391
|
+
description,
|
|
1392
|
+
skillContent: content
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
return commands;
|
|
1396
|
+
}
|
|
1397
|
+
/** Load hooks from hooks/hooks.json if present. */
|
|
1398
|
+
loadHooks(pluginDir) {
|
|
1399
|
+
const hooksPath = join7(pluginDir, "hooks", "hooks.json");
|
|
1400
|
+
if (!existsSync6(hooksPath)) return {};
|
|
1401
|
+
try {
|
|
1402
|
+
const raw = readFileSync6(hooksPath, "utf-8");
|
|
1403
|
+
const data = JSON.parse(raw);
|
|
1404
|
+
if (typeof data === "object" && data !== null) {
|
|
1405
|
+
return data;
|
|
1406
|
+
}
|
|
1407
|
+
return {};
|
|
1408
|
+
} catch {
|
|
1409
|
+
return {};
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
/** Load MCP server configuration if present. Checks `.mcp.json` at plugin root first. */
|
|
1413
|
+
loadMcpConfig(pluginDir) {
|
|
1414
|
+
const primaryPath = join7(pluginDir, ".mcp.json");
|
|
1415
|
+
const fallbackPath = join7(pluginDir, ".claude-plugin", "mcp.json");
|
|
1416
|
+
const mcpPath = existsSync6(primaryPath) ? primaryPath : fallbackPath;
|
|
1417
|
+
if (!existsSync6(mcpPath)) return void 0;
|
|
1418
|
+
try {
|
|
1419
|
+
const raw = readFileSync6(mcpPath, "utf-8");
|
|
1420
|
+
return JSON.parse(raw);
|
|
1421
|
+
} catch {
|
|
1422
|
+
return void 0;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
/** Load agent definitions from agents/ directory if present. */
|
|
1426
|
+
loadAgents(pluginDir) {
|
|
1427
|
+
const agentsDir = join7(pluginDir, "agents");
|
|
1428
|
+
if (!existsSync6(agentsDir)) return [];
|
|
1429
|
+
try {
|
|
1430
|
+
const entries = readdirSync2(agentsDir, { withFileTypes: true });
|
|
1431
|
+
return entries.filter((e) => e.isDirectory() || e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, ""));
|
|
1432
|
+
} catch {
|
|
1433
|
+
return [];
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
};
|
|
1437
|
+
|
|
1438
|
+
// src/plugins/bundle-plugin-installer.ts
|
|
1439
|
+
import { execSync } from "child_process";
|
|
1440
|
+
import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1441
|
+
import { join as join8, dirname as dirname3 } from "path";
|
|
1442
|
+
var GIT_CLONE_TIMEOUT_MS = 6e4;
|
|
1443
|
+
var BundlePluginInstaller = class {
|
|
1444
|
+
pluginsDir;
|
|
1445
|
+
cacheDir;
|
|
1446
|
+
registryPath;
|
|
1447
|
+
settingsStore;
|
|
1448
|
+
marketplaceClient;
|
|
1449
|
+
exec;
|
|
1450
|
+
constructor(options) {
|
|
1451
|
+
this.pluginsDir = options.pluginsDir;
|
|
1452
|
+
this.cacheDir = join8(this.pluginsDir, "cache");
|
|
1453
|
+
this.registryPath = join8(this.pluginsDir, "installed_plugins.json");
|
|
1454
|
+
this.settingsStore = options.settingsStore;
|
|
1455
|
+
this.marketplaceClient = options.marketplaceClient;
|
|
1456
|
+
this.exec = options.exec ?? this.defaultExec;
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Install a plugin from a marketplace.
|
|
1460
|
+
*
|
|
1461
|
+
* 1. Read marketplace manifest to find the plugin entry.
|
|
1462
|
+
* 2. Resolve source (relative path, github, or url).
|
|
1463
|
+
* 3. Copy/clone to `cache/<marketplace>/<plugin>/<version>/`.
|
|
1464
|
+
* 4. Record in `installed_plugins.json`.
|
|
1465
|
+
*/
|
|
1466
|
+
async install(pluginName, marketplaceName) {
|
|
1467
|
+
const manifest = this.marketplaceClient.fetchManifest(marketplaceName);
|
|
1468
|
+
const entry = manifest.plugins.find((p) => p.name === pluginName);
|
|
1469
|
+
if (!entry) {
|
|
1470
|
+
throw new Error(`Plugin "${pluginName}" not found in marketplace "${marketplaceName}"`);
|
|
1471
|
+
}
|
|
1472
|
+
const version = this.resolveVersion(entry, marketplaceName);
|
|
1473
|
+
const targetDir = join8(this.cacheDir, marketplaceName, pluginName, version);
|
|
1474
|
+
if (existsSync7(targetDir)) {
|
|
1475
|
+
throw new Error(
|
|
1476
|
+
`Plugin "${pluginName}" version "${version}" is already installed from "${marketplaceName}"`
|
|
1477
|
+
);
|
|
1478
|
+
}
|
|
1479
|
+
this.resolveAndInstall(entry.source, marketplaceName, pluginName, targetDir);
|
|
1480
|
+
const pluginId = `${pluginName}@${marketplaceName}`;
|
|
1481
|
+
const registry = this.readRegistry();
|
|
1482
|
+
registry[pluginId] = {
|
|
1483
|
+
pluginName,
|
|
1484
|
+
marketplace: marketplaceName,
|
|
1485
|
+
version,
|
|
1486
|
+
installPath: targetDir,
|
|
1487
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1488
|
+
};
|
|
1489
|
+
this.writeRegistry(registry);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Uninstall a plugin.
|
|
1493
|
+
* Removes from cache and from installed_plugins.json.
|
|
1494
|
+
*/
|
|
1495
|
+
async uninstall(pluginId) {
|
|
1496
|
+
const registry = this.readRegistry();
|
|
1497
|
+
const record = registry[pluginId];
|
|
1498
|
+
if (!record) {
|
|
1499
|
+
throw new Error(`Plugin "${pluginId}" is not installed`);
|
|
1500
|
+
}
|
|
1501
|
+
if (existsSync7(record.installPath)) {
|
|
1502
|
+
rmSync(record.installPath, { recursive: true, force: true });
|
|
1503
|
+
}
|
|
1504
|
+
delete registry[pluginId];
|
|
1505
|
+
this.writeRegistry(registry);
|
|
1506
|
+
this.settingsStore.removePluginEntry(pluginId);
|
|
1507
|
+
}
|
|
1508
|
+
/** Enable a plugin by setting its enabledPlugins entry to true. */
|
|
1509
|
+
async enable(pluginId) {
|
|
1510
|
+
this.settingsStore.setPluginEnabled(pluginId, true);
|
|
1511
|
+
}
|
|
1512
|
+
/** Disable a plugin by setting its enabledPlugins entry to false. */
|
|
1513
|
+
async disable(pluginId) {
|
|
1514
|
+
this.settingsStore.setPluginEnabled(pluginId, false);
|
|
1515
|
+
}
|
|
1516
|
+
/** Get all installed plugins. */
|
|
1517
|
+
getInstalledPlugins() {
|
|
1518
|
+
return this.readRegistry();
|
|
1519
|
+
}
|
|
1520
|
+
/** Get plugins installed from a specific marketplace. */
|
|
1521
|
+
getPluginsByMarketplace(marketplaceName) {
|
|
1522
|
+
const registry = this.readRegistry();
|
|
1523
|
+
return Object.values(registry).filter((r) => r.marketplace === marketplaceName);
|
|
1524
|
+
}
|
|
1525
|
+
// --- Private helpers ---
|
|
1526
|
+
/** Resolve the version for a plugin entry. */
|
|
1527
|
+
resolveVersion(entry, marketplaceName) {
|
|
1528
|
+
const entryWithVersion = entry;
|
|
1529
|
+
if (typeof entryWithVersion.version === "string" && entryWithVersion.version) {
|
|
1530
|
+
return entryWithVersion.version;
|
|
1531
|
+
}
|
|
1532
|
+
return this.marketplaceClient.getMarketplaceSha(marketplaceName);
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Normalize source object — Claude Code manifests use `source` key instead of `type`.
|
|
1536
|
+
* e.g., { source: "url", url: "..." } → { type: "url", url: "..." }
|
|
1537
|
+
*/
|
|
1538
|
+
normalizeSource(source) {
|
|
1539
|
+
if (typeof source === "string") return source;
|
|
1540
|
+
const obj = source;
|
|
1541
|
+
if (!obj.type && typeof obj.source === "string") {
|
|
1542
|
+
return { ...obj, type: obj.source };
|
|
1543
|
+
}
|
|
1544
|
+
return source;
|
|
1545
|
+
}
|
|
1546
|
+
/** Resolve the source and install the plugin. */
|
|
1547
|
+
resolveAndInstall(rawSource, marketplaceName, pluginName, targetDir) {
|
|
1548
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
1549
|
+
const source = this.normalizeSource(rawSource);
|
|
1550
|
+
try {
|
|
1551
|
+
if (typeof source === "string") {
|
|
1552
|
+
const marketplaceDir = this.marketplaceClient.getMarketplaceDir(marketplaceName);
|
|
1553
|
+
const sourcePath = join8(marketplaceDir, source);
|
|
1554
|
+
if (!existsSync7(sourcePath)) {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
`Plugin source path "${source}" not found in marketplace "${marketplaceName}"`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
cpSync(sourcePath, targetDir, { recursive: true });
|
|
1560
|
+
} else if (source.type === "github") {
|
|
1561
|
+
const repoUrl = `https://github.com/${source.repo}.git`;
|
|
1562
|
+
this.cloneToDir(repoUrl, targetDir, pluginName);
|
|
1563
|
+
} else if (source.type === "url" && typeof source.url === "string" && source.url.endsWith(".git")) {
|
|
1564
|
+
this.cloneToDir(source.url, targetDir, pluginName);
|
|
1565
|
+
} else if (source.type === "url") {
|
|
1566
|
+
throw new Error(`URL source "${source.url}" is not a git repository (must end with .git)`);
|
|
1567
|
+
} else {
|
|
1568
|
+
throw new Error(`Unknown source type: ${JSON.stringify(source)}`);
|
|
1569
|
+
}
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
if (existsSync7(targetDir)) {
|
|
1572
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
1573
|
+
}
|
|
1574
|
+
throw err;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
/** Clone a git repository to the target directory. */
|
|
1578
|
+
cloneToDir(repoUrl, targetDir, pluginName) {
|
|
1579
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
1580
|
+
const command = `git clone --depth 1 ${repoUrl} ${targetDir}`;
|
|
1581
|
+
try {
|
|
1582
|
+
this.exec(command, { timeout: GIT_CLONE_TIMEOUT_MS, stdio: "pipe" });
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1585
|
+
throw new Error(`Failed to clone plugin "${pluginName}": ${message}`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
/** Read the installed_plugins.json registry. */
|
|
1589
|
+
readRegistry() {
|
|
1590
|
+
if (!existsSync7(this.registryPath)) {
|
|
1591
|
+
return {};
|
|
1592
|
+
}
|
|
1593
|
+
try {
|
|
1594
|
+
const raw = readFileSync7(this.registryPath, "utf-8");
|
|
1595
|
+
const data = JSON.parse(raw);
|
|
1596
|
+
if (typeof data === "object" && data !== null) {
|
|
1597
|
+
return data;
|
|
1598
|
+
}
|
|
1599
|
+
return {};
|
|
1600
|
+
} catch {
|
|
1601
|
+
return {};
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
/** Write the installed_plugins.json registry. */
|
|
1605
|
+
writeRegistry(registry) {
|
|
1606
|
+
const dir = dirname3(this.registryPath);
|
|
1607
|
+
if (!existsSync7(dir)) {
|
|
1608
|
+
mkdirSync3(dir, { recursive: true });
|
|
1609
|
+
}
|
|
1610
|
+
writeFileSync2(this.registryPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1611
|
+
}
|
|
1612
|
+
/** Default exec implementation using child_process. */
|
|
1613
|
+
defaultExec(command, options) {
|
|
1614
|
+
return execSync(command, { timeout: options.timeout, stdio: "pipe" });
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1617
|
+
|
|
1618
|
+
// src/plugins/marketplace-client.ts
|
|
1619
|
+
import { execSync as execSync2 } from "child_process";
|
|
1620
|
+
import {
|
|
1621
|
+
cpSync as cpSync2,
|
|
1622
|
+
existsSync as existsSync8,
|
|
1623
|
+
mkdirSync as mkdirSync4,
|
|
1624
|
+
readFileSync as readFileSync8,
|
|
1625
|
+
renameSync,
|
|
1626
|
+
rmSync as rmSync2,
|
|
1627
|
+
writeFileSync as writeFileSync3
|
|
1628
|
+
} from "fs";
|
|
1629
|
+
import { join as join9, dirname as dirname4 } from "path";
|
|
1630
|
+
var GIT_TIMEOUT_MS = 6e4;
|
|
1631
|
+
var MarketplaceClient = class {
|
|
1632
|
+
pluginsDir;
|
|
1633
|
+
exec;
|
|
1634
|
+
marketplacesDir;
|
|
1635
|
+
registryPath;
|
|
1636
|
+
constructor(options) {
|
|
1637
|
+
this.pluginsDir = options.pluginsDir;
|
|
1638
|
+
this.exec = options.exec ?? this.defaultExec;
|
|
1639
|
+
this.marketplacesDir = join9(this.pluginsDir, "marketplaces");
|
|
1640
|
+
this.registryPath = join9(this.pluginsDir, "known_marketplaces.json");
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Add a marketplace by cloning its repository.
|
|
1644
|
+
*
|
|
1645
|
+
* 1. Parse source: `owner/repo` string becomes a GitHub source.
|
|
1646
|
+
* 2. Shallow git clone (`--depth 1`) to `marketplaces/<name>/`.
|
|
1647
|
+
* 3. Read `.claude-plugin/marketplace.json` for the `name` field.
|
|
1648
|
+
* 4. Register in `known_marketplaces.json`.
|
|
1649
|
+
*
|
|
1650
|
+
* Returns the registered marketplace name from the manifest.
|
|
1651
|
+
*/
|
|
1652
|
+
addMarketplace(source) {
|
|
1653
|
+
const tempName = "temp-" + Date.now().toString(36);
|
|
1654
|
+
const tempDir = join9(this.marketplacesDir, tempName);
|
|
1655
|
+
mkdirSync4(this.marketplacesDir, { recursive: true });
|
|
1656
|
+
if (source.type === "local") {
|
|
1657
|
+
if (!existsSync8(source.path)) {
|
|
1658
|
+
throw new Error(`Local marketplace path does not exist: ${source.path}`);
|
|
1659
|
+
}
|
|
1660
|
+
cpSync2(source.path, tempDir, { recursive: true });
|
|
1661
|
+
} else {
|
|
1662
|
+
const cloneUrl = this.resolveCloneUrl(source);
|
|
1663
|
+
const command = `git clone --depth 1 ${cloneUrl} ${tempDir}`;
|
|
1664
|
+
try {
|
|
1665
|
+
this.exec(command, { timeout: GIT_TIMEOUT_MS, stdio: "pipe" });
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1668
|
+
throw new Error(`Failed to clone marketplace: ${message}`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
const manifestPath = join9(tempDir, ".claude-plugin", "marketplace.json");
|
|
1672
|
+
if (!existsSync8(manifestPath)) {
|
|
1673
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1674
|
+
throw new Error(
|
|
1675
|
+
source.type === "local" ? "Local directory does not contain .claude-plugin/marketplace.json" : "Cloned repository does not contain .claude-plugin/marketplace.json"
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
const manifest = this.readManifestFromPath(manifestPath);
|
|
1679
|
+
const name = manifest.name;
|
|
1680
|
+
if (!name) {
|
|
1681
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1682
|
+
throw new Error('Marketplace manifest does not contain a "name" field');
|
|
1683
|
+
}
|
|
1684
|
+
const registry = this.readRegistry();
|
|
1685
|
+
if (registry[name]) {
|
|
1686
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
1687
|
+
throw new Error(`Marketplace "${name}" already exists`);
|
|
1688
|
+
}
|
|
1689
|
+
const finalDir = join9(this.marketplacesDir, name);
|
|
1690
|
+
renameSync(tempDir, finalDir);
|
|
1691
|
+
registry[name] = {
|
|
1692
|
+
source,
|
|
1693
|
+
installLocation: finalDir,
|
|
1694
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1695
|
+
};
|
|
1696
|
+
this.writeRegistry(registry);
|
|
1697
|
+
return name;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Remove a marketplace.
|
|
1701
|
+
* Uninstalls all plugins from that marketplace, then deletes the clone directory
|
|
1702
|
+
* and removes from the registry.
|
|
1703
|
+
*/
|
|
1704
|
+
removeMarketplace(name) {
|
|
1705
|
+
const registry = this.readRegistry();
|
|
1706
|
+
const entry = registry[name];
|
|
1707
|
+
if (!entry) {
|
|
1708
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1709
|
+
}
|
|
1710
|
+
this.removeInstalledPluginsForMarketplace(name);
|
|
1711
|
+
if (existsSync8(entry.installLocation)) {
|
|
1712
|
+
rmSync2(entry.installLocation, { recursive: true, force: true });
|
|
1713
|
+
}
|
|
1714
|
+
delete registry[name];
|
|
1715
|
+
this.writeRegistry(registry);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Update a marketplace by running git pull on its clone.
|
|
1719
|
+
* The manifest is re-read from disk on demand (via fetchManifest), so the
|
|
1720
|
+
* updated manifest is automatically available after pull.
|
|
1721
|
+
*
|
|
1722
|
+
* TODO: After pull, detect version changes in installed plugins and offer
|
|
1723
|
+
* to update them (re-install at new version).
|
|
1724
|
+
*/
|
|
1725
|
+
updateMarketplace(name) {
|
|
1726
|
+
const registry = this.readRegistry();
|
|
1727
|
+
const entry = registry[name];
|
|
1728
|
+
if (!entry) {
|
|
1729
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1730
|
+
}
|
|
1731
|
+
if (!existsSync8(entry.installLocation)) {
|
|
1732
|
+
throw new Error(`Marketplace directory for "${name}" does not exist`);
|
|
1733
|
+
}
|
|
1734
|
+
if (entry.source.type === "local") {
|
|
1735
|
+
const localSource = entry.source;
|
|
1736
|
+
if (!existsSync8(localSource.path)) {
|
|
1737
|
+
throw new Error(`Local marketplace path does not exist: ${localSource.path}`);
|
|
1738
|
+
}
|
|
1739
|
+
rmSync2(entry.installLocation, { recursive: true, force: true });
|
|
1740
|
+
cpSync2(localSource.path, entry.installLocation, { recursive: true });
|
|
1741
|
+
} else {
|
|
1742
|
+
const command = `git -C ${entry.installLocation} pull`;
|
|
1743
|
+
try {
|
|
1744
|
+
this.exec(command, { timeout: GIT_TIMEOUT_MS, stdio: "pipe" });
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1747
|
+
throw new Error(`Failed to update marketplace "${name}": ${message}`);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
entry.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
1751
|
+
this.writeRegistry(registry);
|
|
1752
|
+
}
|
|
1753
|
+
/** List all registered marketplaces. */
|
|
1754
|
+
listMarketplaces() {
|
|
1755
|
+
const registry = this.readRegistry();
|
|
1756
|
+
return Object.entries(registry).map(([name, entry]) => ({
|
|
1757
|
+
name,
|
|
1758
|
+
source: entry.source,
|
|
1759
|
+
lastUpdated: entry.lastUpdated
|
|
1760
|
+
}));
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Read the marketplace manifest from a registered marketplace's clone.
|
|
1764
|
+
*/
|
|
1765
|
+
fetchManifest(marketplaceName) {
|
|
1766
|
+
const registry = this.readRegistry();
|
|
1767
|
+
const entry = registry[marketplaceName];
|
|
1768
|
+
if (!entry) {
|
|
1769
|
+
throw new Error(`Marketplace "${marketplaceName}" not found`);
|
|
1770
|
+
}
|
|
1771
|
+
const manifestPath = join9(entry.installLocation, ".claude-plugin", "marketplace.json");
|
|
1772
|
+
if (!existsSync8(manifestPath)) {
|
|
1773
|
+
throw new Error(
|
|
1774
|
+
`Marketplace "${marketplaceName}" does not contain .claude-plugin/marketplace.json`
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
return this.readManifestFromPath(manifestPath);
|
|
1778
|
+
}
|
|
1779
|
+
/** Get the clone directory path for a registered marketplace. */
|
|
1780
|
+
getMarketplaceDir(name) {
|
|
1781
|
+
const registry = this.readRegistry();
|
|
1782
|
+
const entry = registry[name];
|
|
1783
|
+
if (!entry) {
|
|
1784
|
+
throw new Error(`Marketplace "${name}" not found`);
|
|
1785
|
+
}
|
|
1786
|
+
return entry.installLocation;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Get the current git SHA (first 12 chars) for a marketplace clone.
|
|
1790
|
+
* Used as a version identifier when plugins lack explicit versions.
|
|
1791
|
+
*/
|
|
1792
|
+
getMarketplaceSha(name) {
|
|
1793
|
+
const dir = this.getMarketplaceDir(name);
|
|
1794
|
+
try {
|
|
1795
|
+
const result = this.exec(`git -C ${dir} rev-parse HEAD`, {
|
|
1796
|
+
timeout: GIT_TIMEOUT_MS,
|
|
1797
|
+
stdio: "pipe"
|
|
1798
|
+
});
|
|
1799
|
+
return result.toString().trim().slice(0, 12);
|
|
1800
|
+
} catch {
|
|
1801
|
+
return "unknown";
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
/** List all available plugins across all marketplaces. */
|
|
1805
|
+
listAvailablePlugins() {
|
|
1806
|
+
const results = [];
|
|
1807
|
+
const marketplaces = this.listMarketplaces();
|
|
1808
|
+
for (const { name } of marketplaces) {
|
|
1809
|
+
try {
|
|
1810
|
+
const manifest = this.fetchManifest(name);
|
|
1811
|
+
for (const plugin of manifest.plugins) {
|
|
1812
|
+
results.push({ ...plugin, marketplace: name });
|
|
1813
|
+
}
|
|
1814
|
+
} catch {
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return results;
|
|
1818
|
+
}
|
|
1819
|
+
// --- Private helpers ---
|
|
1820
|
+
/** Resolve a marketplace source to a git clone URL. */
|
|
1821
|
+
resolveCloneUrl(source) {
|
|
1822
|
+
switch (source.type) {
|
|
1823
|
+
case "github":
|
|
1824
|
+
return `https://github.com/${source.repo}.git`;
|
|
1825
|
+
case "git":
|
|
1826
|
+
return source.url;
|
|
1827
|
+
case "local":
|
|
1828
|
+
throw new Error("Local source type does not use git cloning");
|
|
1829
|
+
case "url":
|
|
1830
|
+
throw new Error("URL marketplace source is not yet supported");
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* Remove all installed plugins that belong to a given marketplace.
|
|
1835
|
+
* Reads installed_plugins.json, deletes cache directories for matching plugins,
|
|
1836
|
+
* and updates the registry.
|
|
1837
|
+
*/
|
|
1838
|
+
removeInstalledPluginsForMarketplace(marketplaceName) {
|
|
1839
|
+
const installedPath = join9(this.pluginsDir, "installed_plugins.json");
|
|
1840
|
+
if (!existsSync8(installedPath)) return;
|
|
1841
|
+
let registry;
|
|
1842
|
+
try {
|
|
1843
|
+
const raw = readFileSync8(installedPath, "utf-8");
|
|
1844
|
+
const data = JSON.parse(raw);
|
|
1845
|
+
if (typeof data !== "object" || data === null) return;
|
|
1846
|
+
registry = data;
|
|
1847
|
+
} catch {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
let changed = false;
|
|
1851
|
+
for (const [pluginId, record] of Object.entries(registry)) {
|
|
1852
|
+
if (record.marketplace === marketplaceName) {
|
|
1853
|
+
if (record.installPath && existsSync8(record.installPath)) {
|
|
1854
|
+
rmSync2(record.installPath, { recursive: true, force: true });
|
|
1855
|
+
}
|
|
1856
|
+
delete registry[pluginId];
|
|
1857
|
+
changed = true;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (changed) {
|
|
1861
|
+
const dir = dirname4(installedPath);
|
|
1862
|
+
if (!existsSync8(dir)) {
|
|
1863
|
+
mkdirSync4(dir, { recursive: true });
|
|
1864
|
+
}
|
|
1865
|
+
writeFileSync3(installedPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
/** Read and parse a marketplace.json from a file path. */
|
|
1869
|
+
readManifestFromPath(path) {
|
|
1870
|
+
const raw = readFileSync8(path, "utf-8");
|
|
1871
|
+
const data = JSON.parse(raw);
|
|
1872
|
+
if (typeof data !== "object" || data === null) {
|
|
1873
|
+
throw new Error("Invalid marketplace manifest: not an object");
|
|
1874
|
+
}
|
|
1875
|
+
const obj = data;
|
|
1876
|
+
if (typeof obj.name !== "string") {
|
|
1877
|
+
throw new Error('Invalid marketplace manifest: missing "name" field');
|
|
1878
|
+
}
|
|
1879
|
+
return data;
|
|
1880
|
+
}
|
|
1881
|
+
/** Read the known_marketplaces.json registry. */
|
|
1882
|
+
readRegistry() {
|
|
1883
|
+
if (!existsSync8(this.registryPath)) {
|
|
1884
|
+
return {};
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
const raw = readFileSync8(this.registryPath, "utf-8");
|
|
1888
|
+
const data = JSON.parse(raw);
|
|
1889
|
+
if (typeof data === "object" && data !== null) {
|
|
1890
|
+
return data;
|
|
1891
|
+
}
|
|
1892
|
+
return {};
|
|
1893
|
+
} catch {
|
|
1894
|
+
return {};
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
/** Write the known_marketplaces.json registry. */
|
|
1898
|
+
writeRegistry(registry) {
|
|
1899
|
+
const dir = dirname4(this.registryPath);
|
|
1900
|
+
if (!existsSync8(dir)) {
|
|
1901
|
+
mkdirSync4(dir, { recursive: true });
|
|
1902
|
+
}
|
|
1903
|
+
writeFileSync3(this.registryPath, JSON.stringify(registry, null, 2), "utf-8");
|
|
1904
|
+
}
|
|
1905
|
+
/** Default exec implementation using child_process. */
|
|
1906
|
+
defaultExec(command, options) {
|
|
1907
|
+
return execSync2(command, { timeout: options.timeout, stdio: "pipe" });
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
|
|
1911
|
+
// src/commands/command-registry.ts
|
|
1912
|
+
var CommandRegistry = class {
|
|
1913
|
+
sources = [];
|
|
1914
|
+
addSource(source) {
|
|
1915
|
+
this.sources.push(source);
|
|
1916
|
+
}
|
|
1917
|
+
/** Get all commands, optionally filtered by prefix */
|
|
1918
|
+
getCommands(filter) {
|
|
1919
|
+
const all = [];
|
|
1920
|
+
for (const source of this.sources) {
|
|
1921
|
+
all.push(...source.getCommands());
|
|
1922
|
+
}
|
|
1923
|
+
if (!filter) return all;
|
|
1924
|
+
const lower = filter.toLowerCase();
|
|
1925
|
+
return all.filter((cmd) => cmd.name.toLowerCase().startsWith(lower));
|
|
1926
|
+
}
|
|
1927
|
+
/** Resolve a short name to its fully qualified plugin:name form */
|
|
1928
|
+
resolveQualifiedName(shortName) {
|
|
1929
|
+
const matches = this.getCommands().filter(
|
|
1930
|
+
(c) => c.source === "plugin" && c.name.includes(":") && c.name.endsWith(`:${shortName}`)
|
|
1931
|
+
);
|
|
1932
|
+
if (matches.length !== 1) return null;
|
|
1933
|
+
return matches[0].name;
|
|
1934
|
+
}
|
|
1935
|
+
/** Get subcommands for a specific command */
|
|
1936
|
+
getSubcommands(commandName) {
|
|
1937
|
+
const lower = commandName.toLowerCase();
|
|
1938
|
+
for (const source of this.sources) {
|
|
1939
|
+
for (const cmd of source.getCommands()) {
|
|
1940
|
+
if (cmd.name.toLowerCase() === lower && cmd.subcommands) {
|
|
1941
|
+
return cmd.subcommands;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return [];
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1949
|
+
// src/commands/builtin-source.ts
|
|
1950
|
+
import { CLAUDE_MODELS, formatTokenCount } from "@robota-sdk/agent-core";
|
|
1951
|
+
function buildModelSubcommands() {
|
|
1952
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1953
|
+
const commands = [];
|
|
1954
|
+
for (const model of Object.values(CLAUDE_MODELS)) {
|
|
1955
|
+
if (seen.has(model.name)) continue;
|
|
1956
|
+
seen.add(model.name);
|
|
1957
|
+
commands.push({
|
|
1958
|
+
name: model.id,
|
|
1959
|
+
description: `${model.name} (${formatTokenCount(model.contextWindow).toUpperCase()})`,
|
|
1960
|
+
source: "builtin"
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
return commands;
|
|
1964
|
+
}
|
|
1965
|
+
function createBuiltinCommands() {
|
|
1966
|
+
return [
|
|
1967
|
+
{ name: "help", description: "Show available commands", source: "builtin" },
|
|
1968
|
+
{ name: "clear", description: "Clear conversation history", source: "builtin" },
|
|
1969
|
+
{
|
|
1970
|
+
name: "mode",
|
|
1971
|
+
description: "Permission mode",
|
|
1972
|
+
source: "builtin",
|
|
1973
|
+
subcommands: [
|
|
1974
|
+
{ name: "plan", description: "Plan only, no execution", source: "builtin" },
|
|
1975
|
+
{ name: "default", description: "Ask before risky actions", source: "builtin" },
|
|
1976
|
+
{ name: "acceptEdits", description: "Auto-approve file edits", source: "builtin" },
|
|
1977
|
+
{ name: "bypassPermissions", description: "Skip all permission checks", source: "builtin" }
|
|
1978
|
+
]
|
|
533
1979
|
},
|
|
534
|
-
|
|
1980
|
+
{
|
|
1981
|
+
name: "model",
|
|
1982
|
+
description: "Select AI model",
|
|
1983
|
+
source: "builtin",
|
|
1984
|
+
subcommands: buildModelSubcommands()
|
|
535
1985
|
},
|
|
536
|
-
|
|
1986
|
+
{
|
|
1987
|
+
name: "language",
|
|
1988
|
+
description: "Set response language",
|
|
1989
|
+
source: "builtin",
|
|
1990
|
+
subcommands: [
|
|
1991
|
+
{ name: "ko", description: "Korean", source: "builtin" },
|
|
1992
|
+
{ name: "en", description: "English", source: "builtin" },
|
|
1993
|
+
{ name: "ja", description: "Japanese", source: "builtin" },
|
|
1994
|
+
{ name: "zh", description: "Chinese", source: "builtin" }
|
|
1995
|
+
]
|
|
537
1996
|
},
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1997
|
+
{ name: "compact", description: "Compress context window", source: "builtin" },
|
|
1998
|
+
{ name: "cost", description: "Show session info", source: "builtin" },
|
|
1999
|
+
{ name: "context", description: "Context window info", source: "builtin" },
|
|
2000
|
+
{ name: "permissions", description: "Permission rules", source: "builtin" },
|
|
2001
|
+
{ name: "plugin", description: "Manage plugins", source: "builtin" },
|
|
2002
|
+
{ name: "reload-plugins", description: "Reload all plugin resources", source: "builtin" },
|
|
2003
|
+
{ name: "reset", description: "Delete settings and exit", source: "builtin" },
|
|
2004
|
+
{ name: "exit", description: "Exit CLI", source: "builtin" }
|
|
2005
|
+
];
|
|
2006
|
+
}
|
|
2007
|
+
var BuiltinCommandSource = class {
|
|
2008
|
+
name = "builtin";
|
|
2009
|
+
commands;
|
|
2010
|
+
constructor() {
|
|
2011
|
+
this.commands = createBuiltinCommands();
|
|
2012
|
+
}
|
|
2013
|
+
getCommands() {
|
|
2014
|
+
return this.commands;
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
// src/commands/skill-source.ts
|
|
2019
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
|
|
2020
|
+
import { join as join10, basename as basename2 } from "path";
|
|
2021
|
+
import { homedir as homedir3 } from "os";
|
|
2022
|
+
var BOOLEAN_KEYS = /* @__PURE__ */ new Set(["disable-model-invocation", "user-invocable"]);
|
|
2023
|
+
var LIST_KEYS2 = /* @__PURE__ */ new Set(["allowed-tools"]);
|
|
2024
|
+
function kebabToCamel(key) {
|
|
2025
|
+
return key.replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
2026
|
+
}
|
|
2027
|
+
function parseFrontmatter2(content) {
|
|
2028
|
+
const lines = content.split("\n");
|
|
2029
|
+
if (lines[0]?.trim() !== "---") return null;
|
|
2030
|
+
const result = {};
|
|
2031
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2032
|
+
const line = lines[i];
|
|
2033
|
+
if (line.trim() === "---") break;
|
|
2034
|
+
const match = line.match(/^([a-z][a-z0-9-]*):\s*(.+)/);
|
|
2035
|
+
if (!match) continue;
|
|
2036
|
+
const key = match[1];
|
|
2037
|
+
const rawValue = match[2].trim();
|
|
2038
|
+
const camelKey = kebabToCamel(key);
|
|
2039
|
+
if (BOOLEAN_KEYS.has(key)) {
|
|
2040
|
+
result[camelKey] = rawValue === "true";
|
|
2041
|
+
} else if (LIST_KEYS2.has(key)) {
|
|
2042
|
+
result[camelKey] = rawValue.split(",").map((s) => s.trim());
|
|
2043
|
+
} else {
|
|
2044
|
+
result[camelKey] = rawValue;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
2048
|
+
}
|
|
2049
|
+
function buildCommand(frontmatter, content, fallbackName) {
|
|
2050
|
+
const cmd = {
|
|
2051
|
+
name: frontmatter?.name ?? fallbackName,
|
|
2052
|
+
description: frontmatter?.description ?? `Skill: ${fallbackName}`,
|
|
2053
|
+
source: "skill",
|
|
2054
|
+
skillContent: content
|
|
543
2055
|
};
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
2056
|
+
if (frontmatter?.argumentHint !== void 0) cmd.argumentHint = frontmatter.argumentHint;
|
|
2057
|
+
if (frontmatter?.disableModelInvocation !== void 0)
|
|
2058
|
+
cmd.disableModelInvocation = frontmatter.disableModelInvocation;
|
|
2059
|
+
if (frontmatter?.userInvocable !== void 0) cmd.userInvocable = frontmatter.userInvocable;
|
|
2060
|
+
if (frontmatter?.allowedTools !== void 0) cmd.allowedTools = frontmatter.allowedTools;
|
|
2061
|
+
if (frontmatter?.model !== void 0) cmd.model = frontmatter.model;
|
|
2062
|
+
if (frontmatter?.effort !== void 0) cmd.effort = frontmatter.effort;
|
|
2063
|
+
if (frontmatter?.context !== void 0) cmd.context = frontmatter.context;
|
|
2064
|
+
if (frontmatter?.agent !== void 0) cmd.agent = frontmatter.agent;
|
|
2065
|
+
return cmd;
|
|
2066
|
+
}
|
|
2067
|
+
function scanSkillsDir(skillsDir) {
|
|
2068
|
+
if (!existsSync9(skillsDir)) return [];
|
|
2069
|
+
const commands = [];
|
|
2070
|
+
const entries = readdirSync3(skillsDir, { withFileTypes: true });
|
|
2071
|
+
for (const entry of entries) {
|
|
2072
|
+
if (!entry.isDirectory()) continue;
|
|
2073
|
+
const skillFile = join10(skillsDir, entry.name, "SKILL.md");
|
|
2074
|
+
if (!existsSync9(skillFile)) continue;
|
|
2075
|
+
const content = readFileSync9(skillFile, "utf-8");
|
|
2076
|
+
const frontmatter = parseFrontmatter2(content);
|
|
2077
|
+
commands.push(buildCommand(frontmatter, content, entry.name));
|
|
2078
|
+
}
|
|
2079
|
+
return commands;
|
|
2080
|
+
}
|
|
2081
|
+
function scanCommandsDir(commandsDir) {
|
|
2082
|
+
if (!existsSync9(commandsDir)) return [];
|
|
2083
|
+
const commands = [];
|
|
2084
|
+
const entries = readdirSync3(commandsDir, { withFileTypes: true });
|
|
2085
|
+
for (const entry of entries) {
|
|
2086
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
2087
|
+
const filePath = join10(commandsDir, entry.name);
|
|
2088
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
2089
|
+
const frontmatter = parseFrontmatter2(content);
|
|
2090
|
+
const fallbackName = basename2(entry.name, ".md");
|
|
2091
|
+
commands.push(buildCommand(frontmatter, content, fallbackName));
|
|
2092
|
+
}
|
|
2093
|
+
return commands;
|
|
2094
|
+
}
|
|
2095
|
+
var SkillCommandSource = class {
|
|
2096
|
+
name = "skill";
|
|
2097
|
+
cwd;
|
|
2098
|
+
home;
|
|
2099
|
+
cachedCommands = null;
|
|
2100
|
+
constructor(cwd, home) {
|
|
2101
|
+
this.cwd = cwd;
|
|
2102
|
+
this.home = home ?? homedir3();
|
|
2103
|
+
}
|
|
2104
|
+
getCommands() {
|
|
2105
|
+
if (this.cachedCommands) return this.cachedCommands;
|
|
2106
|
+
const sources = [
|
|
2107
|
+
scanSkillsDir(join10(this.cwd, ".claude", "skills")),
|
|
2108
|
+
scanCommandsDir(join10(this.cwd, ".claude", "commands")),
|
|
2109
|
+
scanSkillsDir(join10(this.home, ".robota", "skills")),
|
|
2110
|
+
scanSkillsDir(join10(this.cwd, ".agents", "skills"))
|
|
2111
|
+
];
|
|
2112
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2113
|
+
const merged = [];
|
|
2114
|
+
for (const commands of sources) {
|
|
2115
|
+
for (const cmd of commands) {
|
|
2116
|
+
if (!seen.has(cmd.name)) {
|
|
2117
|
+
seen.add(cmd.name);
|
|
2118
|
+
merged.push(cmd);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
this.cachedCommands = merged;
|
|
2123
|
+
return this.cachedCommands;
|
|
2124
|
+
}
|
|
2125
|
+
getModelInvocableSkills() {
|
|
2126
|
+
return this.getCommands().filter((cmd) => cmd.disableModelInvocation !== true);
|
|
2127
|
+
}
|
|
2128
|
+
getUserInvocableSkills() {
|
|
2129
|
+
return this.getCommands().filter((cmd) => cmd.userInvocable !== false);
|
|
2130
|
+
}
|
|
2131
|
+
};
|
|
2132
|
+
|
|
2133
|
+
// src/commands/system-command.ts
|
|
2134
|
+
var VALID_MODES = ["plan", "default", "acceptEdits", "bypassPermissions"];
|
|
2135
|
+
function createSystemCommands() {
|
|
2136
|
+
return [
|
|
2137
|
+
{
|
|
2138
|
+
name: "help",
|
|
2139
|
+
description: "Show available commands",
|
|
2140
|
+
execute: (_session, _args) => ({
|
|
2141
|
+
message: [
|
|
2142
|
+
"Available commands:",
|
|
2143
|
+
" help \u2014 Show this help",
|
|
2144
|
+
" clear \u2014 Clear conversation",
|
|
2145
|
+
" compact [instr] \u2014 Compact context (optional focus instructions)",
|
|
2146
|
+
" mode [m] \u2014 Show/change permission mode",
|
|
2147
|
+
" model <id> \u2014 Change AI model",
|
|
2148
|
+
" language <code> \u2014 Set response language (ko, en, ja, zh)",
|
|
2149
|
+
" cost \u2014 Show session info",
|
|
2150
|
+
" context \u2014 Context window info",
|
|
2151
|
+
" permissions \u2014 Permission rules",
|
|
2152
|
+
" reset \u2014 Delete settings and exit"
|
|
2153
|
+
].join("\n"),
|
|
2154
|
+
success: true
|
|
2155
|
+
})
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
name: "clear",
|
|
2159
|
+
description: "Clear conversation history",
|
|
2160
|
+
execute: (session, _args) => {
|
|
2161
|
+
const underlying = session.getSession();
|
|
2162
|
+
underlying.clearHistory();
|
|
2163
|
+
return { message: "Conversation cleared.", success: true };
|
|
2164
|
+
}
|
|
2165
|
+
},
|
|
2166
|
+
{
|
|
2167
|
+
name: "compact",
|
|
2168
|
+
description: "Compress context window",
|
|
2169
|
+
execute: async (session, args) => {
|
|
2170
|
+
const underlying = session.getSession();
|
|
2171
|
+
const instructions = args.trim() || void 0;
|
|
2172
|
+
const before = underlying.getContextState().usedPercentage;
|
|
2173
|
+
await underlying.compact(instructions);
|
|
2174
|
+
const after = underlying.getContextState().usedPercentage;
|
|
2175
|
+
return {
|
|
2176
|
+
message: `Context compacted: ${Math.round(before)}% -> ${Math.round(after)}%`,
|
|
2177
|
+
success: true,
|
|
2178
|
+
data: { before, after }
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
name: "mode",
|
|
2184
|
+
description: "Show/change permission mode",
|
|
2185
|
+
execute: (session, args) => {
|
|
2186
|
+
const underlying = session.getSession();
|
|
2187
|
+
const arg = args.trim().split(/\s+/)[0];
|
|
2188
|
+
if (!arg) {
|
|
2189
|
+
return {
|
|
2190
|
+
message: `Current mode: ${underlying.getPermissionMode()}`,
|
|
2191
|
+
success: true,
|
|
2192
|
+
data: { mode: underlying.getPermissionMode() }
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
if (VALID_MODES.includes(arg)) {
|
|
2196
|
+
underlying.setPermissionMode(arg);
|
|
2197
|
+
return {
|
|
2198
|
+
message: `Permission mode set to: ${arg}`,
|
|
2199
|
+
success: true,
|
|
2200
|
+
data: { mode: arg }
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
return {
|
|
2204
|
+
message: `Invalid mode. Valid: ${VALID_MODES.join(" | ")}`,
|
|
2205
|
+
success: false
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
},
|
|
2209
|
+
{
|
|
2210
|
+
name: "model",
|
|
2211
|
+
description: "Change AI model",
|
|
2212
|
+
execute: (_session, args) => {
|
|
2213
|
+
const modelId = args.trim().split(/\s+/)[0];
|
|
2214
|
+
if (!modelId) {
|
|
2215
|
+
return { message: "Usage: model <model-id>", success: false };
|
|
2216
|
+
}
|
|
2217
|
+
return {
|
|
2218
|
+
message: `Model change requested: ${modelId}`,
|
|
2219
|
+
success: true,
|
|
2220
|
+
data: { modelId }
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
},
|
|
2224
|
+
{
|
|
2225
|
+
name: "language",
|
|
2226
|
+
description: "Set response language",
|
|
2227
|
+
execute: (_session, args) => {
|
|
2228
|
+
const lang = args.trim().split(/\s+/)[0];
|
|
2229
|
+
if (!lang) {
|
|
2230
|
+
return { message: "Usage: language <code> (e.g., ko, en, ja, zh)", success: false };
|
|
2231
|
+
}
|
|
2232
|
+
return {
|
|
2233
|
+
message: `Language set to "${lang}".`,
|
|
2234
|
+
success: true,
|
|
2235
|
+
data: { language: lang }
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
{
|
|
2240
|
+
name: "cost",
|
|
2241
|
+
description: "Show session info",
|
|
2242
|
+
execute: (session, _args) => {
|
|
2243
|
+
const underlying = session.getSession();
|
|
2244
|
+
const sessionId = underlying.getSessionId();
|
|
2245
|
+
const messageCount = underlying.getMessageCount();
|
|
2246
|
+
return {
|
|
2247
|
+
message: `Session: ${sessionId}
|
|
2248
|
+
Messages: ${messageCount}`,
|
|
2249
|
+
success: true,
|
|
2250
|
+
data: { sessionId, messageCount }
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
},
|
|
2254
|
+
{
|
|
2255
|
+
name: "context",
|
|
2256
|
+
description: "Context window info",
|
|
2257
|
+
execute: (session, _args) => {
|
|
2258
|
+
const ctx = session.getContextState();
|
|
2259
|
+
return {
|
|
2260
|
+
message: `Context: ${ctx.usedTokens.toLocaleString()} / ${ctx.maxTokens.toLocaleString()} tokens (${Math.round(ctx.usedPercentage)}%)`,
|
|
2261
|
+
success: true,
|
|
2262
|
+
data: {
|
|
2263
|
+
usedTokens: ctx.usedTokens,
|
|
2264
|
+
maxTokens: ctx.maxTokens,
|
|
2265
|
+
percentage: ctx.usedPercentage
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
name: "permissions",
|
|
2272
|
+
description: "Show permission rules",
|
|
2273
|
+
execute: (session, _args) => {
|
|
2274
|
+
const underlying = session.getSession();
|
|
2275
|
+
const mode = underlying.getPermissionMode();
|
|
2276
|
+
const sessionAllowed = underlying.getSessionAllowedTools();
|
|
2277
|
+
const lines = [`Permission mode: ${mode}`];
|
|
2278
|
+
if (sessionAllowed.length > 0) {
|
|
2279
|
+
lines.push(`Session-approved tools: ${sessionAllowed.join(", ")}`);
|
|
2280
|
+
} else {
|
|
2281
|
+
lines.push("No session-approved tools.");
|
|
2282
|
+
}
|
|
2283
|
+
return {
|
|
2284
|
+
message: lines.join("\n"),
|
|
2285
|
+
success: true,
|
|
2286
|
+
data: { mode, sessionAllowed }
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
},
|
|
2290
|
+
{
|
|
2291
|
+
name: "reset",
|
|
2292
|
+
description: "Delete settings",
|
|
2293
|
+
execute: (_session, _args) => {
|
|
2294
|
+
return {
|
|
2295
|
+
message: "Reset requested.",
|
|
2296
|
+
success: true,
|
|
2297
|
+
data: { resetRequested: true }
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
];
|
|
2302
|
+
}
|
|
2303
|
+
var SystemCommandExecutor = class {
|
|
2304
|
+
commands;
|
|
2305
|
+
constructor(commands) {
|
|
2306
|
+
this.commands = /* @__PURE__ */ new Map();
|
|
2307
|
+
for (const cmd of commands ?? createSystemCommands()) {
|
|
2308
|
+
this.commands.set(cmd.name, cmd);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
/** Register an additional command. */
|
|
2312
|
+
register(command) {
|
|
2313
|
+
this.commands.set(command.name, command);
|
|
2314
|
+
}
|
|
2315
|
+
/** Execute a command by name. Returns null if command not found. */
|
|
2316
|
+
async execute(name, session, args) {
|
|
2317
|
+
const cmd = this.commands.get(name);
|
|
2318
|
+
if (!cmd) return null;
|
|
2319
|
+
return await cmd.execute(session, args);
|
|
2320
|
+
}
|
|
2321
|
+
/** List all registered commands. */
|
|
2322
|
+
listCommands() {
|
|
2323
|
+
return [...this.commands.values()];
|
|
2324
|
+
}
|
|
2325
|
+
/** Check if a command exists. */
|
|
2326
|
+
hasCommand(name) {
|
|
2327
|
+
return this.commands.has(name);
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
|
|
2331
|
+
// src/interactive/interactive-session.ts
|
|
2332
|
+
import { FileSessionLogger as FileSessionLogger2 } from "@robota-sdk/agent-sessions";
|
|
2333
|
+
import {
|
|
2334
|
+
createUserMessage,
|
|
2335
|
+
createAssistantMessage,
|
|
2336
|
+
createSystemMessage
|
|
2337
|
+
} from "@robota-sdk/agent-core";
|
|
2338
|
+
var TOOL_ARG_DISPLAY_MAX = 80;
|
|
2339
|
+
var TAIL_KEEP = 30;
|
|
2340
|
+
var MAX_COMPLETED_TOOLS = 50;
|
|
2341
|
+
var STREAMING_FLUSH_INTERVAL_MS = 16;
|
|
2342
|
+
var InteractiveSession = class {
|
|
2343
|
+
session;
|
|
2344
|
+
listeners = /* @__PURE__ */ new Map();
|
|
2345
|
+
// Streaming state
|
|
2346
|
+
streamingText = "";
|
|
2347
|
+
flushTimer = null;
|
|
2348
|
+
// Tool state
|
|
2349
|
+
activeTools = [];
|
|
2350
|
+
// Execution state
|
|
2351
|
+
executing = false;
|
|
2352
|
+
pendingPrompt = null;
|
|
2353
|
+
pendingDisplayInput;
|
|
2354
|
+
pendingRawInput;
|
|
2355
|
+
// Display messages (what clients render — not the raw session history)
|
|
2356
|
+
messages = [];
|
|
2357
|
+
constructor(options) {
|
|
2358
|
+
if (options.session) {
|
|
2359
|
+
this.session = options.session;
|
|
2360
|
+
} else {
|
|
2361
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2362
|
+
const paths = projectPaths(cwd);
|
|
2363
|
+
this.session = createSession({
|
|
2364
|
+
config: options.config,
|
|
2365
|
+
context: options.context,
|
|
2366
|
+
projectInfo: options.projectInfo,
|
|
2367
|
+
sessionStore: options.sessionStore,
|
|
2368
|
+
permissionMode: options.permissionMode,
|
|
2369
|
+
maxTurns: options.maxTurns,
|
|
2370
|
+
terminal: NOOP_TERMINAL,
|
|
2371
|
+
sessionLogger: new FileSessionLogger2(paths.logs),
|
|
2372
|
+
permissionHandler: options.permissionHandler,
|
|
2373
|
+
onTextDelta: (delta) => this.handleTextDelta(delta),
|
|
2374
|
+
onToolExecution: (event) => this.handleToolExecution(event)
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
// ── Event system ──────────────────────────────────────────────
|
|
2379
|
+
on(event, handler) {
|
|
2380
|
+
if (!this.listeners.has(event)) {
|
|
2381
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
2382
|
+
}
|
|
2383
|
+
this.listeners.get(event).add(handler);
|
|
2384
|
+
}
|
|
2385
|
+
off(event, handler) {
|
|
2386
|
+
this.listeners.get(event)?.delete(handler);
|
|
2387
|
+
}
|
|
2388
|
+
emit(event, ...args) {
|
|
2389
|
+
const handlers = this.listeners.get(event);
|
|
2390
|
+
if (handlers) {
|
|
2391
|
+
for (const handler of handlers) {
|
|
2392
|
+
handler(...args);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
// ── Public API ────────────────────────────────────────────────
|
|
2397
|
+
/** Submit a prompt. Queues if already executing (max 1 queued).
|
|
2398
|
+
* displayInput overrides what appears as the user message (e.g., "/audit" instead of full skill prompt).
|
|
2399
|
+
* rawInput is passed to Session.run() for hook matching (e.g., "/rulebased-harness:audit"). */
|
|
2400
|
+
async submit(input, displayInput, rawInput) {
|
|
2401
|
+
if (this.executing) {
|
|
2402
|
+
this.pendingPrompt = input;
|
|
2403
|
+
this.pendingDisplayInput = displayInput;
|
|
2404
|
+
this.pendingRawInput = rawInput;
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
await this.executePrompt(input, displayInput, rawInput);
|
|
2408
|
+
}
|
|
2409
|
+
/** Abort current execution and clear queue. */
|
|
2410
|
+
abort() {
|
|
2411
|
+
this.pendingPrompt = null;
|
|
2412
|
+
this.session.abort();
|
|
2413
|
+
}
|
|
2414
|
+
/** Cancel queued prompt without aborting current execution. */
|
|
2415
|
+
cancelQueue() {
|
|
2416
|
+
this.pendingPrompt = null;
|
|
2417
|
+
}
|
|
2418
|
+
isExecuting() {
|
|
2419
|
+
return this.executing;
|
|
2420
|
+
}
|
|
2421
|
+
getPendingPrompt() {
|
|
2422
|
+
return this.pendingPrompt;
|
|
2423
|
+
}
|
|
2424
|
+
getMessages() {
|
|
2425
|
+
return this.messages;
|
|
2426
|
+
}
|
|
2427
|
+
getStreamingText() {
|
|
2428
|
+
return this.streamingText;
|
|
2429
|
+
}
|
|
2430
|
+
getActiveTools() {
|
|
2431
|
+
return this.activeTools;
|
|
2432
|
+
}
|
|
2433
|
+
getContextState() {
|
|
2434
|
+
return this.session.getContextState();
|
|
2435
|
+
}
|
|
2436
|
+
getSession() {
|
|
2437
|
+
return this.session;
|
|
2438
|
+
}
|
|
2439
|
+
// ── Execution ─────────────────────────────────────────────────
|
|
2440
|
+
async executePrompt(input, displayInput, rawInput) {
|
|
2441
|
+
this.executing = true;
|
|
2442
|
+
this.clearStreaming();
|
|
2443
|
+
this.emit("thinking", true);
|
|
2444
|
+
this.messages.push(createUserMessage(displayInput ?? input));
|
|
2445
|
+
const historyBefore = this.session.getHistory().length;
|
|
2446
|
+
try {
|
|
2447
|
+
const response = await this.session.run(input, rawInput);
|
|
2448
|
+
this.flushStreaming();
|
|
2449
|
+
this.clearStreaming();
|
|
2450
|
+
const result = this.buildResult(response || "(empty response)", historyBefore);
|
|
2451
|
+
this.messages.push(createAssistantMessage(result.response));
|
|
2452
|
+
this.emit("complete", result);
|
|
2453
|
+
this.emit("context_update", this.getContextState());
|
|
2454
|
+
} catch (err) {
|
|
2455
|
+
this.flushStreaming();
|
|
2456
|
+
this.clearStreaming();
|
|
2457
|
+
if (isAbortError(err)) {
|
|
2458
|
+
const result = this.buildInterruptedResult(historyBefore);
|
|
2459
|
+
if (result.response) {
|
|
2460
|
+
this.messages.push(createAssistantMessage(result.response));
|
|
2461
|
+
}
|
|
2462
|
+
this.messages.push(createSystemMessage("Interrupted by user."));
|
|
2463
|
+
this.emit("interrupted", result);
|
|
2464
|
+
} else {
|
|
2465
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2466
|
+
this.messages.push(createSystemMessage(`Error: ${errMsg}`));
|
|
2467
|
+
this.emit("error", err instanceof Error ? err : new Error(errMsg));
|
|
2468
|
+
}
|
|
2469
|
+
} finally {
|
|
2470
|
+
this.executing = false;
|
|
2471
|
+
this.emit("thinking", false);
|
|
2472
|
+
if (this.pendingPrompt) {
|
|
2473
|
+
const queued = this.pendingPrompt;
|
|
2474
|
+
const queuedDisplay = this.pendingDisplayInput;
|
|
2475
|
+
const queuedRaw = this.pendingRawInput;
|
|
2476
|
+
this.pendingPrompt = null;
|
|
2477
|
+
this.pendingDisplayInput = void 0;
|
|
2478
|
+
this.pendingRawInput = void 0;
|
|
2479
|
+
setTimeout(() => this.executePrompt(queued, queuedDisplay, queuedRaw), 0);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
// ── Streaming callbacks ───────────────────────────────────────
|
|
2484
|
+
handleTextDelta(delta) {
|
|
2485
|
+
this.streamingText += delta;
|
|
2486
|
+
this.emit("text_delta", delta);
|
|
2487
|
+
if (!this.flushTimer) {
|
|
2488
|
+
this.flushTimer = setTimeout(() => {
|
|
2489
|
+
this.flushTimer = null;
|
|
2490
|
+
}, STREAMING_FLUSH_INTERVAL_MS);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
handleToolExecution(event) {
|
|
2494
|
+
if (event.type === "start") {
|
|
2495
|
+
const firstArg = extractFirstArg(event.toolArgs);
|
|
2496
|
+
const state = {
|
|
2497
|
+
toolName: event.toolName,
|
|
2498
|
+
firstArg,
|
|
2499
|
+
isRunning: true
|
|
2500
|
+
};
|
|
2501
|
+
this.activeTools.push(state);
|
|
2502
|
+
this.emit("tool_start", state);
|
|
2503
|
+
} else {
|
|
2504
|
+
const result = event.denied ? "denied" : event.success === false ? "error" : "success";
|
|
2505
|
+
const idx = this.activeTools.findIndex((t) => t.toolName === event.toolName && t.isRunning);
|
|
2506
|
+
if (idx !== -1) {
|
|
2507
|
+
const finished = {
|
|
2508
|
+
...this.activeTools[idx],
|
|
2509
|
+
isRunning: false,
|
|
2510
|
+
result
|
|
2511
|
+
};
|
|
2512
|
+
this.activeTools[idx] = finished;
|
|
2513
|
+
this.trimCompletedTools();
|
|
2514
|
+
this.emit("tool_end", finished);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
2519
|
+
clearStreaming() {
|
|
2520
|
+
this.streamingText = "";
|
|
2521
|
+
this.activeTools = [];
|
|
2522
|
+
if (this.flushTimer) {
|
|
2523
|
+
clearTimeout(this.flushTimer);
|
|
2524
|
+
this.flushTimer = null;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
flushStreaming() {
|
|
2528
|
+
if (this.flushTimer) {
|
|
2529
|
+
clearTimeout(this.flushTimer);
|
|
2530
|
+
this.flushTimer = null;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
buildResult(response, historyBefore) {
|
|
2534
|
+
const toolSummaries = this.extractToolSummaries(historyBefore);
|
|
2535
|
+
return {
|
|
2536
|
+
response,
|
|
2537
|
+
messages: this.messages,
|
|
2538
|
+
toolSummaries,
|
|
2539
|
+
contextState: this.getContextState()
|
|
557
2540
|
};
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
2541
|
+
}
|
|
2542
|
+
buildInterruptedResult(historyBefore) {
|
|
2543
|
+
const history = this.session.getHistory();
|
|
2544
|
+
const toolSummaries = this.extractToolSummaries(historyBefore);
|
|
2545
|
+
const parts = [];
|
|
2546
|
+
for (let i = historyBefore; i < history.length; i++) {
|
|
2547
|
+
const msg = history[i];
|
|
2548
|
+
if (msg?.role === "assistant" && msg.content) {
|
|
2549
|
+
parts.push(msg.content);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
return {
|
|
2553
|
+
response: parts.join("\n\n"),
|
|
2554
|
+
messages: this.messages,
|
|
2555
|
+
toolSummaries,
|
|
2556
|
+
contextState: this.getContextState()
|
|
565
2557
|
};
|
|
566
|
-
return JSON.stringify(result);
|
|
567
2558
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
2559
|
+
extractToolSummaries(historyBefore) {
|
|
2560
|
+
const history = this.session.getHistory();
|
|
2561
|
+
const summaries = [];
|
|
2562
|
+
for (let i = historyBefore; i < history.length; i++) {
|
|
2563
|
+
const msg = history[i];
|
|
2564
|
+
if (msg?.role === "assistant" && msg.toolCalls) {
|
|
2565
|
+
for (const tc of msg.toolCalls) {
|
|
2566
|
+
summaries.push({ name: tc.function.name, args: tc.function.arguments });
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
return summaries;
|
|
575
2571
|
}
|
|
576
|
-
)
|
|
2572
|
+
trimCompletedTools() {
|
|
2573
|
+
const completed = this.activeTools.filter((t) => !t.isRunning);
|
|
2574
|
+
if (completed.length > MAX_COMPLETED_TOOLS) {
|
|
2575
|
+
const excess = completed.length - MAX_COMPLETED_TOOLS;
|
|
2576
|
+
let removed = 0;
|
|
2577
|
+
this.activeTools = this.activeTools.filter((t) => {
|
|
2578
|
+
if (!t.isRunning && removed < excess) {
|
|
2579
|
+
removed++;
|
|
2580
|
+
return false;
|
|
2581
|
+
}
|
|
2582
|
+
return true;
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
};
|
|
2587
|
+
function isAbortError(err) {
|
|
2588
|
+
return err instanceof DOMException && err.name === "AbortError" || err instanceof Error && (err.message.includes("aborted") || err.message.includes("abort"));
|
|
2589
|
+
}
|
|
2590
|
+
function extractFirstArg(toolArgs) {
|
|
2591
|
+
if (!toolArgs) return "";
|
|
2592
|
+
const firstVal = Object.values(toolArgs)[0];
|
|
2593
|
+
const raw = typeof firstVal === "string" ? firstVal : JSON.stringify(firstVal ?? "");
|
|
2594
|
+
return raw.length > TOOL_ARG_DISPLAY_MAX ? raw.slice(0, TOOL_ARG_DISPLAY_MAX - TAIL_KEEP - 3) + "..." + raw.slice(-TAIL_KEEP) : raw;
|
|
2595
|
+
}
|
|
2596
|
+
var NOOP_TERMINAL = {
|
|
2597
|
+
write: () => {
|
|
2598
|
+
},
|
|
2599
|
+
writeLine: () => {
|
|
2600
|
+
},
|
|
2601
|
+
writeMarkdown: () => {
|
|
2602
|
+
},
|
|
2603
|
+
writeError: () => {
|
|
2604
|
+
},
|
|
2605
|
+
prompt: () => Promise.resolve(""),
|
|
2606
|
+
select: () => Promise.resolve(0),
|
|
2607
|
+
spinner: () => ({ stop: () => {
|
|
2608
|
+
}, update: () => {
|
|
2609
|
+
} })
|
|
2610
|
+
};
|
|
577
2611
|
|
|
578
2612
|
// src/index.ts
|
|
579
2613
|
import { bashTool as bashTool2 } from "@robota-sdk/agent-tools";
|
|
@@ -583,31 +2617,53 @@ import { editTool as editTool2 } from "@robota-sdk/agent-tools";
|
|
|
583
2617
|
import { globTool as globTool2 } from "@robota-sdk/agent-tools";
|
|
584
2618
|
import { grepTool as grepTool2 } from "@robota-sdk/agent-tools";
|
|
585
2619
|
export {
|
|
2620
|
+
AgentExecutor,
|
|
2621
|
+
BUILT_IN_AGENTS,
|
|
2622
|
+
BuiltinCommandSource,
|
|
2623
|
+
BundlePluginInstaller,
|
|
2624
|
+
BundlePluginLoader,
|
|
2625
|
+
CommandRegistry,
|
|
586
2626
|
DEFAULT_TOOL_DESCRIPTIONS,
|
|
587
|
-
FileSessionLogger,
|
|
588
|
-
|
|
2627
|
+
FileSessionLogger3 as FileSessionLogger,
|
|
2628
|
+
InteractiveSession,
|
|
2629
|
+
MarketplaceClient,
|
|
2630
|
+
PluginSettingsStore,
|
|
2631
|
+
PromptExecutor,
|
|
2632
|
+
Session3 as Session,
|
|
589
2633
|
SessionStore,
|
|
590
2634
|
SilentSessionLogger,
|
|
2635
|
+
SkillCommandSource,
|
|
2636
|
+
SystemCommandExecutor,
|
|
591
2637
|
TRUST_TO_MODE,
|
|
592
|
-
|
|
2638
|
+
assembleSubagentPrompt,
|
|
593
2639
|
bashTool2 as bashTool,
|
|
594
2640
|
buildSystemPrompt,
|
|
2641
|
+
createAgentTool,
|
|
595
2642
|
createDefaultTools,
|
|
596
2643
|
createProvider,
|
|
597
2644
|
createSession,
|
|
2645
|
+
createSubagentLogger,
|
|
2646
|
+
createSubagentSession,
|
|
2647
|
+
createSystemCommands,
|
|
598
2648
|
detectProject,
|
|
599
2649
|
editTool2 as editTool,
|
|
600
2650
|
evaluatePermission,
|
|
2651
|
+
getBuiltInAgent,
|
|
2652
|
+
getForkWorkerSuffix,
|
|
2653
|
+
getSubagentSuffix,
|
|
601
2654
|
globTool2 as globTool,
|
|
602
2655
|
grepTool2 as grepTool,
|
|
603
2656
|
loadConfig,
|
|
604
2657
|
loadContext,
|
|
2658
|
+
parseFrontmatter2 as parseFrontmatter,
|
|
605
2659
|
projectPaths,
|
|
606
2660
|
promptForApproval,
|
|
607
2661
|
query,
|
|
608
2662
|
readTool2 as readTool,
|
|
2663
|
+
resolveSubagentLogDir,
|
|
2664
|
+
retrieveAgentToolDeps,
|
|
609
2665
|
runHooks,
|
|
610
|
-
|
|
2666
|
+
storeAgentToolDeps,
|
|
611
2667
|
userPaths,
|
|
612
2668
|
writeTool2 as writeTool
|
|
613
2669
|
};
|