@web42/cli 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/search.js +20 -15
- package/dist/commands/send.js +75 -41
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +116 -213
- package/dist/index.js +1 -19
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.js +0 -27
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.js +0 -451
- package/dist/commands/install.d.ts +0 -3
- package/dist/commands/install.js +0 -231
- package/dist/commands/list.d.ts +0 -3
- package/dist/commands/list.js +0 -22
- package/dist/commands/pack.d.ts +0 -2
- package/dist/commands/pack.js +0 -210
- package/dist/commands/pull.d.ts +0 -2
- package/dist/commands/pull.js +0 -202
- package/dist/commands/push.d.ts +0 -2
- package/dist/commands/push.js +0 -374
- package/dist/commands/remix.d.ts +0 -2
- package/dist/commands/remix.js +0 -49
- package/dist/commands/sync.d.ts +0 -2
- package/dist/commands/sync.js +0 -98
- package/dist/commands/uninstall.d.ts +0 -3
- package/dist/commands/uninstall.js +0 -54
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.js +0 -59
- package/dist/platforms/base.d.ts +0 -82
- package/dist/platforms/base.js +0 -1
- package/dist/platforms/claude/__tests__/adapter.test.d.ts +0 -1
- package/dist/platforms/claude/__tests__/adapter.test.js +0 -257
- package/dist/platforms/claude/__tests__/security.test.d.ts +0 -1
- package/dist/platforms/claude/__tests__/security.test.js +0 -166
- package/dist/platforms/claude/adapter.d.ts +0 -34
- package/dist/platforms/claude/adapter.js +0 -525
- package/dist/platforms/claude/security.d.ts +0 -15
- package/dist/platforms/claude/security.js +0 -67
- package/dist/platforms/claude/templates.d.ts +0 -5
- package/dist/platforms/claude/templates.js +0 -22
- package/dist/platforms/openclaw/adapter.d.ts +0 -12
- package/dist/platforms/openclaw/adapter.js +0 -476
- package/dist/platforms/openclaw/templates.d.ts +0 -7
- package/dist/platforms/openclaw/templates.js +0 -369
- package/dist/platforms/registry.d.ts +0 -6
- package/dist/platforms/registry.js +0 -32
- package/dist/types/sync.d.ts +0 -74
- package/dist/types/sync.js +0 -7
- package/dist/utils/bundled-skills.d.ts +0 -6
- package/dist/utils/bundled-skills.js +0 -29
- package/dist/utils/secrets.d.ts +0 -32
- package/dist/utils/secrets.js +0 -118
- package/dist/utils/skill.d.ts +0 -6
- package/dist/utils/skill.js +0 -42
- package/dist/utils/sync.d.ts +0 -14
- package/dist/utils/sync.js +0 -242
|
@@ -1,525 +0,0 @@
|
|
|
1
|
-
import { createHash } from "crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "fs";
|
|
3
|
-
import { dirname, join, resolve } from "path";
|
|
4
|
-
import { homedir } from "os";
|
|
5
|
-
import { glob } from "glob";
|
|
6
|
-
import { stripForbiddenFrontmatter } from "./security.js";
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Constants
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
const CLAUDE_HOME = join(homedir(), ".claude");
|
|
11
|
-
const HARDCODED_EXCLUDES = [
|
|
12
|
-
".claude/settings*",
|
|
13
|
-
".claude/settings.json",
|
|
14
|
-
".claude/settings.local.json",
|
|
15
|
-
".claude/projects/**",
|
|
16
|
-
".claude/plugins/**",
|
|
17
|
-
"memory/**",
|
|
18
|
-
"MEMORY.md",
|
|
19
|
-
".git/**",
|
|
20
|
-
"node_modules/**",
|
|
21
|
-
".DS_Store",
|
|
22
|
-
"*.log",
|
|
23
|
-
".web42/**",
|
|
24
|
-
".web42ignore",
|
|
25
|
-
"manifest.json",
|
|
26
|
-
".env",
|
|
27
|
-
".env.*",
|
|
28
|
-
];
|
|
29
|
-
const TEMPLATE_VARS = [
|
|
30
|
-
[/\/Users\/[^/]+\/.claude/g, "{{CLAUDE_HOME}}"],
|
|
31
|
-
[/\/home\/[^/]+\/.claude/g, "{{CLAUDE_HOME}}"],
|
|
32
|
-
[/C:\\Users\\[^\\]+\\.claude/g, "{{CLAUDE_HOME}}"],
|
|
33
|
-
[/~\/.claude/g, "{{CLAUDE_HOME}}"],
|
|
34
|
-
];
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Helpers
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
function sanitizeContent(content) {
|
|
39
|
-
let result = content;
|
|
40
|
-
for (const [pattern, replacement] of TEMPLATE_VARS) {
|
|
41
|
-
result = result.replace(pattern, replacement);
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
44
|
-
}
|
|
45
|
-
function hashContent(content) {
|
|
46
|
-
return createHash("sha256").update(content).digest("hex");
|
|
47
|
-
}
|
|
48
|
-
function resolveTemplateVars(content) {
|
|
49
|
-
return content.replace(/\{\{CLAUDE_HOME\}\}/g, CLAUDE_HOME);
|
|
50
|
-
}
|
|
51
|
-
function replaceConfigPlaceholders(content, answers) {
|
|
52
|
-
let result = content;
|
|
53
|
-
for (const [key, value] of Object.entries(answers)) {
|
|
54
|
-
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Parse YAML-like frontmatter from an agent .md file.
|
|
60
|
-
* Handles simple key: value pairs and list values (skills, tools).
|
|
61
|
-
*/
|
|
62
|
-
function parseAgentFrontmatter(content) {
|
|
63
|
-
const lines = content.split("\n");
|
|
64
|
-
if (lines[0]?.trim() !== "---")
|
|
65
|
-
return {};
|
|
66
|
-
const result = {};
|
|
67
|
-
let currentKey = null;
|
|
68
|
-
for (let i = 1; i < lines.length; i++) {
|
|
69
|
-
const line = lines[i];
|
|
70
|
-
if (line.trim() === "---")
|
|
71
|
-
break;
|
|
72
|
-
// Top-level key
|
|
73
|
-
const keyMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
|
|
74
|
-
if (keyMatch) {
|
|
75
|
-
const key = keyMatch[1];
|
|
76
|
-
const value = keyMatch[2].trim();
|
|
77
|
-
if (value) {
|
|
78
|
-
result[key] = value.replace(/^["']|["']$/g, "");
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
result[key] = [];
|
|
82
|
-
}
|
|
83
|
-
currentKey = key;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
// List item (indented - item)
|
|
87
|
-
const listMatch = line.match(/^\s+-\s+(.+)$/) || line.match(/^\s+- (.+)$/);
|
|
88
|
-
if (listMatch && currentKey) {
|
|
89
|
-
const arr = result[currentKey];
|
|
90
|
-
if (Array.isArray(arr)) {
|
|
91
|
-
arr.push(listMatch[1].trim().replace(/^["']|["']$/g, ""));
|
|
92
|
-
}
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
// Indented continuation — could be multiline value
|
|
96
|
-
if (line.match(/^\s+/) && currentKey) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
function readInstalledManifest(root) {
|
|
103
|
-
const base = root ?? CLAUDE_HOME;
|
|
104
|
-
const p = join(base, ".web42", "installed.json");
|
|
105
|
-
if (!existsSync(p))
|
|
106
|
-
return {};
|
|
107
|
-
try {
|
|
108
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return {};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
function writeInstalledManifest(root, manifest) {
|
|
115
|
-
const dir = join(root, ".web42");
|
|
116
|
-
mkdirSync(dir, { recursive: true });
|
|
117
|
-
writeFileSync(join(dir, "installed.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* Check if a file was installed by web42 (tracked in installed.json).
|
|
121
|
-
*/
|
|
122
|
-
function isFileTracked(relativePath, installed) {
|
|
123
|
-
for (const [agentName, entry] of Object.entries(installed)) {
|
|
124
|
-
if (entry.files.includes(relativePath)) {
|
|
125
|
-
return agentName;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// Adapter
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
export class ClaudeAdapter {
|
|
134
|
-
name = "claude";
|
|
135
|
-
home = CLAUDE_HOME;
|
|
136
|
-
/**
|
|
137
|
-
* Discover agent .md files in known locations.
|
|
138
|
-
*/
|
|
139
|
-
discoverAgents(cwd) {
|
|
140
|
-
const candidates = [];
|
|
141
|
-
const seen = new Set();
|
|
142
|
-
const searchDirs = [];
|
|
143
|
-
// 1. cwd/agents/ (standalone plugin dir or ~/.claude/)
|
|
144
|
-
const cwdAgentsDir = join(cwd, "agents");
|
|
145
|
-
if (existsSync(cwdAgentsDir)) {
|
|
146
|
-
searchDirs.push(cwdAgentsDir);
|
|
147
|
-
}
|
|
148
|
-
// 2. cwd/.claude/agents/ (project-local agents)
|
|
149
|
-
const projectAgentsDir = join(cwd, ".claude", "agents");
|
|
150
|
-
if (existsSync(projectAgentsDir)) {
|
|
151
|
-
searchDirs.push(projectAgentsDir);
|
|
152
|
-
}
|
|
153
|
-
// 3. ~/.claude/agents/ (global, if cwd isn't already ~/.claude/)
|
|
154
|
-
// Use `this.home` so tests can override the home dir for isolation.
|
|
155
|
-
const globalAgentsDir = join(this.home, "agents");
|
|
156
|
-
if (resolve(cwd) !== resolve(this.home) && existsSync(globalAgentsDir)) {
|
|
157
|
-
searchDirs.push(globalAgentsDir);
|
|
158
|
-
}
|
|
159
|
-
for (const dir of searchDirs) {
|
|
160
|
-
try {
|
|
161
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
162
|
-
for (const entry of entries) {
|
|
163
|
-
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
164
|
-
continue;
|
|
165
|
-
const agentPath = join(dir, entry.name);
|
|
166
|
-
const content = readFileSync(agentPath, "utf-8");
|
|
167
|
-
const frontmatter = parseAgentFrontmatter(content);
|
|
168
|
-
const name = (typeof frontmatter.name === "string" ? frontmatter.name : null)
|
|
169
|
-
?? entry.name.replace(/\.md$/, "");
|
|
170
|
-
if (seen.has(name))
|
|
171
|
-
continue;
|
|
172
|
-
seen.add(name);
|
|
173
|
-
const skills = Array.isArray(frontmatter.skills)
|
|
174
|
-
? frontmatter.skills
|
|
175
|
-
: [];
|
|
176
|
-
candidates.push({
|
|
177
|
-
name,
|
|
178
|
-
description: typeof frontmatter.description === "string"
|
|
179
|
-
? frontmatter.description
|
|
180
|
-
: undefined,
|
|
181
|
-
model: typeof frontmatter.model === "string"
|
|
182
|
-
? frontmatter.model
|
|
183
|
-
: undefined,
|
|
184
|
-
skills,
|
|
185
|
-
path: agentPath,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
// Directory not readable, skip
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return candidates;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Resolve skill directories from known locations.
|
|
197
|
-
*/
|
|
198
|
-
resolveSkills(skillNames, cwd) {
|
|
199
|
-
return skillNames.map((name) => {
|
|
200
|
-
const searchPaths = [
|
|
201
|
-
join(cwd, "skills", name, "SKILL.md"),
|
|
202
|
-
join(CLAUDE_HOME, "skills", name, "SKILL.md"),
|
|
203
|
-
join(cwd, ".claude", "skills", name, "SKILL.md"),
|
|
204
|
-
];
|
|
205
|
-
for (const p of searchPaths) {
|
|
206
|
-
if (existsSync(p)) {
|
|
207
|
-
return { name, sourcePath: dirname(p), found: true };
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return { name, sourcePath: "", found: false };
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
extractInitConfig(cwd) {
|
|
214
|
-
const agents = this.discoverAgents(cwd);
|
|
215
|
-
if (agents.length === 0)
|
|
216
|
-
return null;
|
|
217
|
-
const first = agents[0];
|
|
218
|
-
return {
|
|
219
|
-
name: first.name,
|
|
220
|
-
model: first.model || undefined,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
async pack(options) {
|
|
224
|
-
const { cwd, agentName } = options;
|
|
225
|
-
if (!agentName) {
|
|
226
|
-
throw new Error("Claude adapter requires agentName in PackOptions. " +
|
|
227
|
-
"Use --agent <name> or let the pack command auto-discover agents.");
|
|
228
|
-
}
|
|
229
|
-
// Read the agent .md file (check cwd/agents/ and cwd/.claude/agents/)
|
|
230
|
-
let agentMdPath = join(cwd, "agents", `${agentName}.md`);
|
|
231
|
-
if (!existsSync(agentMdPath)) {
|
|
232
|
-
agentMdPath = join(cwd, ".claude", "agents", `${agentName}.md`);
|
|
233
|
-
}
|
|
234
|
-
if (!existsSync(agentMdPath)) {
|
|
235
|
-
throw new Error(`Agent file not found: agents/${agentName}.md or .claude/agents/${agentName}.md`);
|
|
236
|
-
}
|
|
237
|
-
const agentContent = readFileSync(agentMdPath, "utf-8");
|
|
238
|
-
const frontmatter = parseAgentFrontmatter(agentContent);
|
|
239
|
-
const skillNames = Array.isArray(frontmatter.skills) ? frontmatter.skills : [];
|
|
240
|
-
// Resolve skills
|
|
241
|
-
const resolvedSkills = this.resolveSkills(skillNames, cwd);
|
|
242
|
-
const missingSkills = resolvedSkills.filter((s) => !s.found);
|
|
243
|
-
if (missingSkills.length > 0) {
|
|
244
|
-
const names = missingSkills.map((s) => s.name).join(", ");
|
|
245
|
-
console.warn(`Warning: Skills not found: ${names}`);
|
|
246
|
-
}
|
|
247
|
-
// Collect ignore patterns
|
|
248
|
-
const ignorePatterns = [...HARDCODED_EXCLUDES];
|
|
249
|
-
const web42Dir = join(cwd, ".web42");
|
|
250
|
-
const agentWeb42Dir = join(web42Dir, agentName);
|
|
251
|
-
const ignorePath = join(agentWeb42Dir, ".web42ignore");
|
|
252
|
-
if (existsSync(ignorePath)) {
|
|
253
|
-
const ignoreContent = readFileSync(ignorePath, "utf-8");
|
|
254
|
-
ignoreContent
|
|
255
|
-
.split("\n")
|
|
256
|
-
.map((line) => line.trim())
|
|
257
|
-
.filter((line) => line && !line.startsWith("#"))
|
|
258
|
-
.forEach((pattern) => ignorePatterns.push(pattern));
|
|
259
|
-
}
|
|
260
|
-
const files = [];
|
|
261
|
-
// 1. Add the agent .md itself
|
|
262
|
-
const sanitizedAgent = sanitizeContent(agentContent);
|
|
263
|
-
files.push({
|
|
264
|
-
path: `agents/${agentName}.md`,
|
|
265
|
-
content: sanitizedAgent,
|
|
266
|
-
hash: hashContent(sanitizedAgent),
|
|
267
|
-
});
|
|
268
|
-
// 2. Add each resolved skill's full directory
|
|
269
|
-
for (const skill of resolvedSkills) {
|
|
270
|
-
if (!skill.found)
|
|
271
|
-
continue;
|
|
272
|
-
const skillDir = skill.sourcePath;
|
|
273
|
-
const skillFiles = await glob("**/*", {
|
|
274
|
-
cwd: skillDir,
|
|
275
|
-
nodir: true,
|
|
276
|
-
ignore: ignorePatterns,
|
|
277
|
-
dot: true,
|
|
278
|
-
});
|
|
279
|
-
for (const filePath of skillFiles) {
|
|
280
|
-
const fullPath = join(skillDir, filePath);
|
|
281
|
-
const stat = statSync(fullPath);
|
|
282
|
-
if (stat.size > 1024 * 1024)
|
|
283
|
-
continue;
|
|
284
|
-
try {
|
|
285
|
-
let content = readFileSync(fullPath, "utf-8");
|
|
286
|
-
content = sanitizeContent(content);
|
|
287
|
-
files.push({
|
|
288
|
-
path: `skills/${skill.name}/${filePath}`,
|
|
289
|
-
content,
|
|
290
|
-
hash: hashContent(content),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
// Skip binary files
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
// 3. Add commands/*.md if present (check cwd/commands/ and cwd/.claude/commands/)
|
|
299
|
-
const commandsDirs = [
|
|
300
|
-
join(cwd, "commands"),
|
|
301
|
-
join(cwd, ".claude", "commands"),
|
|
302
|
-
];
|
|
303
|
-
const seenCommands = new Set();
|
|
304
|
-
for (const commandsDir of commandsDirs) {
|
|
305
|
-
if (!existsSync(commandsDir))
|
|
306
|
-
continue;
|
|
307
|
-
try {
|
|
308
|
-
const commandFiles = readdirSync(commandsDir, { withFileTypes: true });
|
|
309
|
-
for (const entry of commandFiles) {
|
|
310
|
-
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
311
|
-
continue;
|
|
312
|
-
if (seenCommands.has(entry.name))
|
|
313
|
-
continue;
|
|
314
|
-
seenCommands.add(entry.name);
|
|
315
|
-
const fullPath = join(commandsDir, entry.name);
|
|
316
|
-
const stat = statSync(fullPath);
|
|
317
|
-
if (stat.size > 1024 * 1024)
|
|
318
|
-
continue;
|
|
319
|
-
try {
|
|
320
|
-
let content = readFileSync(fullPath, "utf-8");
|
|
321
|
-
content = sanitizeContent(content);
|
|
322
|
-
files.push({
|
|
323
|
-
path: `commands/${entry.name}`,
|
|
324
|
-
content,
|
|
325
|
-
hash: hashContent(content),
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
catch {
|
|
329
|
-
// Skip
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
catch {
|
|
334
|
-
// Commands dir not readable
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
// 4. Add scripts/** if present (check BOTH cwd/scripts/ and cwd/.claude/scripts/)
|
|
338
|
-
const scriptsDir = join(cwd, "scripts");
|
|
339
|
-
const claudeScriptsDir = join(cwd, ".claude", "scripts");
|
|
340
|
-
const allScriptFiles = new Map(); // filePath -> content
|
|
341
|
-
if (existsSync(scriptsDir)) {
|
|
342
|
-
const scriptFiles = await glob("**/*", {
|
|
343
|
-
cwd: scriptsDir,
|
|
344
|
-
nodir: true,
|
|
345
|
-
ignore: ignorePatterns,
|
|
346
|
-
dot: true,
|
|
347
|
-
});
|
|
348
|
-
for (const filePath of scriptFiles) {
|
|
349
|
-
try {
|
|
350
|
-
const fullPath = join(scriptsDir, filePath);
|
|
351
|
-
const stat = statSync(fullPath);
|
|
352
|
-
if (stat.size > 1024 * 1024)
|
|
353
|
-
continue;
|
|
354
|
-
let content = readFileSync(fullPath, "utf-8");
|
|
355
|
-
content = sanitizeContent(content);
|
|
356
|
-
if (!allScriptFiles.has(filePath)) {
|
|
357
|
-
allScriptFiles.set(filePath, content);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
catch {
|
|
361
|
-
// Skip
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
if (existsSync(claudeScriptsDir)) {
|
|
366
|
-
const claudeScriptFiles = await glob("**/*", {
|
|
367
|
-
cwd: claudeScriptsDir,
|
|
368
|
-
nodir: true,
|
|
369
|
-
ignore: ignorePatterns,
|
|
370
|
-
dot: true,
|
|
371
|
-
});
|
|
372
|
-
for (const filePath of claudeScriptFiles) {
|
|
373
|
-
try {
|
|
374
|
-
const fullPath = join(claudeScriptsDir, filePath);
|
|
375
|
-
const stat = statSync(fullPath);
|
|
376
|
-
if (stat.size > 1024 * 1024)
|
|
377
|
-
continue;
|
|
378
|
-
let content = readFileSync(fullPath, "utf-8");
|
|
379
|
-
content = sanitizeContent(content);
|
|
380
|
-
if (!allScriptFiles.has(filePath)) {
|
|
381
|
-
allScriptFiles.set(filePath, content);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
// Skip
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
for (const [filePath, content] of allScriptFiles.entries()) {
|
|
390
|
-
files.push({
|
|
391
|
-
path: `scripts/${filePath}`,
|
|
392
|
-
content,
|
|
393
|
-
hash: hashContent(content),
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
// Detect config variables from {{VAR}} patterns in agent .md
|
|
397
|
-
const configVariables = [];
|
|
398
|
-
const varPattern = /\{\{([A-Z_][A-Z0-9_]*)\}\}/g;
|
|
399
|
-
const reservedVars = new Set(["CLAUDE_HOME", "WORKSPACE"]);
|
|
400
|
-
let match;
|
|
401
|
-
while ((match = varPattern.exec(agentContent)) !== null) {
|
|
402
|
-
if (!reservedVars.has(match[1])) {
|
|
403
|
-
configVariables.push({
|
|
404
|
-
key: match[1],
|
|
405
|
-
label: match[1].replace(/_/g, " ").toLowerCase(),
|
|
406
|
-
required: true,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
return {
|
|
411
|
-
files,
|
|
412
|
-
configTemplate: null,
|
|
413
|
-
configVariables,
|
|
414
|
-
ignorePatterns,
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
async install(options) {
|
|
418
|
-
const { agentSlug, files, configAnswers, workspacePath } = options;
|
|
419
|
-
// workspacePath determines where files land:
|
|
420
|
-
// local install → cwd/.claude/ (default)
|
|
421
|
-
// global install → ~/.claude/ (with -g flag)
|
|
422
|
-
const targetRoot = workspacePath;
|
|
423
|
-
const installed = readInstalledManifest(targetRoot);
|
|
424
|
-
const trackedFiles = [];
|
|
425
|
-
let filesWritten = 0;
|
|
426
|
-
for (const file of files) {
|
|
427
|
-
// Validate no path traversal
|
|
428
|
-
const normalized = resolve(targetRoot, file.path);
|
|
429
|
-
if (!normalized.startsWith(resolve(targetRoot))) {
|
|
430
|
-
throw new Error(`Path traversal detected: ${file.path}`);
|
|
431
|
-
}
|
|
432
|
-
// Check for conflicts with non-web42 files
|
|
433
|
-
const targetPath = join(targetRoot, file.path);
|
|
434
|
-
if (existsSync(targetPath)) {
|
|
435
|
-
const tracker = isFileTracked(file.path, installed);
|
|
436
|
-
if (!tracker || tracker !== agentSlug) {
|
|
437
|
-
console.warn(` Warning: Overwriting existing file: ${file.path}`);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
// Security filter for agent .md files
|
|
441
|
-
let content = file.content ?? "";
|
|
442
|
-
if (file.path.startsWith("agents/") && file.path.endsWith(".md")) {
|
|
443
|
-
const { cleaned, stripped } = stripForbiddenFrontmatter(content);
|
|
444
|
-
content = cleaned;
|
|
445
|
-
if (stripped.length > 0) {
|
|
446
|
-
console.warn(` Security: Stripped ${stripped.join(", ")} from ${file.path}`);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
// Resolve template vars and config placeholders
|
|
450
|
-
content = resolveTemplateVars(content);
|
|
451
|
-
content = replaceConfigPlaceholders(content, configAnswers);
|
|
452
|
-
// Write the file
|
|
453
|
-
mkdirSync(dirname(targetPath), { recursive: true });
|
|
454
|
-
writeFileSync(targetPath, content, "utf-8");
|
|
455
|
-
trackedFiles.push(file.path);
|
|
456
|
-
filesWritten++;
|
|
457
|
-
}
|
|
458
|
-
// Update installed.json
|
|
459
|
-
writeInstalledManifest(targetRoot, {
|
|
460
|
-
...readInstalledManifest(targetRoot),
|
|
461
|
-
[agentSlug]: {
|
|
462
|
-
source: `@${options.username}/${agentSlug}`,
|
|
463
|
-
version: options.version ?? "1.0.0",
|
|
464
|
-
installed_at: new Date().toISOString(),
|
|
465
|
-
files: trackedFiles,
|
|
466
|
-
},
|
|
467
|
-
});
|
|
468
|
-
return { filesWritten, agentDir: targetRoot };
|
|
469
|
-
}
|
|
470
|
-
async uninstall(options) {
|
|
471
|
-
const { agentName, workspacePath } = options;
|
|
472
|
-
const targetRoot = workspacePath ?? CLAUDE_HOME;
|
|
473
|
-
const installed = readInstalledManifest(targetRoot);
|
|
474
|
-
const entry = installed[agentName];
|
|
475
|
-
if (!entry) {
|
|
476
|
-
return { removed: false, paths: [] };
|
|
477
|
-
}
|
|
478
|
-
const removedPaths = [];
|
|
479
|
-
// Remove each tracked file
|
|
480
|
-
for (const filePath of entry.files) {
|
|
481
|
-
const fullPath = join(targetRoot, filePath);
|
|
482
|
-
if (existsSync(fullPath)) {
|
|
483
|
-
rmSync(fullPath);
|
|
484
|
-
removedPaths.push(fullPath);
|
|
485
|
-
// Clean up empty parent directories (e.g., empty skill dirs)
|
|
486
|
-
let parentDir = dirname(fullPath);
|
|
487
|
-
while (parentDir !== targetRoot) {
|
|
488
|
-
try {
|
|
489
|
-
const entries = readdirSync(parentDir);
|
|
490
|
-
if (entries.length === 0) {
|
|
491
|
-
rmSync(parentDir, { recursive: true });
|
|
492
|
-
parentDir = dirname(parentDir);
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
catch {
|
|
499
|
-
break;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// Remove from installed.json
|
|
505
|
-
delete installed[agentName];
|
|
506
|
-
writeInstalledManifest(targetRoot, installed);
|
|
507
|
-
return { removed: removedPaths.length > 0, paths: removedPaths };
|
|
508
|
-
}
|
|
509
|
-
async listInstalled(workspacePath) {
|
|
510
|
-
const targetRoot = workspacePath ?? CLAUDE_HOME;
|
|
511
|
-
const installed = readInstalledManifest(targetRoot);
|
|
512
|
-
return Object.entries(installed).map(([name, entry]) => ({
|
|
513
|
-
name,
|
|
514
|
-
source: entry.source,
|
|
515
|
-
workspace: targetRoot,
|
|
516
|
-
}));
|
|
517
|
-
}
|
|
518
|
-
resolveInstallPath(_localName, global) {
|
|
519
|
-
if (global)
|
|
520
|
-
return CLAUDE_HOME;
|
|
521
|
-
return join(process.cwd(), ".claude");
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
export const claudeAdapter = new ClaudeAdapter();
|
|
525
|
-
export { HARDCODED_EXCLUDES as CLAUDE_HARDCODED_EXCLUDES };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security filtering for Claude Code agent .md files.
|
|
3
|
-
*
|
|
4
|
-
* Plugin subagents do not support hooks, mcpServers, or permissionMode.
|
|
5
|
-
* These keys are stripped from frontmatter on install to prevent
|
|
6
|
-
* marketplace-distributed agents from modifying the buyer's security posture.
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* Strip forbidden frontmatter keys from a Claude agent .md file.
|
|
10
|
-
* Returns the cleaned content and a list of keys that were removed.
|
|
11
|
-
*/
|
|
12
|
-
export declare function stripForbiddenFrontmatter(content: string): {
|
|
13
|
-
cleaned: string;
|
|
14
|
-
stripped: string[];
|
|
15
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security filtering for Claude Code agent .md files.
|
|
3
|
-
*
|
|
4
|
-
* Plugin subagents do not support hooks, mcpServers, or permissionMode.
|
|
5
|
-
* These keys are stripped from frontmatter on install to prevent
|
|
6
|
-
* marketplace-distributed agents from modifying the buyer's security posture.
|
|
7
|
-
*/
|
|
8
|
-
const FORBIDDEN_FRONTMATTER_KEYS = new Set([
|
|
9
|
-
"hooks",
|
|
10
|
-
"mcpServers",
|
|
11
|
-
"permissionMode",
|
|
12
|
-
]);
|
|
13
|
-
/**
|
|
14
|
-
* Strip forbidden frontmatter keys from a Claude agent .md file.
|
|
15
|
-
* Returns the cleaned content and a list of keys that were removed.
|
|
16
|
-
*/
|
|
17
|
-
export function stripForbiddenFrontmatter(content) {
|
|
18
|
-
const lines = content.split("\n");
|
|
19
|
-
// No frontmatter — return as-is
|
|
20
|
-
if (lines[0]?.trim() !== "---") {
|
|
21
|
-
return { cleaned: content, stripped: [] };
|
|
22
|
-
}
|
|
23
|
-
// Find the closing ---
|
|
24
|
-
let closingIndex = -1;
|
|
25
|
-
for (let i = 1; i < lines.length; i++) {
|
|
26
|
-
if (lines[i].trim() === "---") {
|
|
27
|
-
closingIndex = i;
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// Malformed frontmatter (no closing ---) — return as-is
|
|
32
|
-
if (closingIndex === -1) {
|
|
33
|
-
return { cleaned: content, stripped: [] };
|
|
34
|
-
}
|
|
35
|
-
const stripped = [];
|
|
36
|
-
const filteredFrontmatterLines = [];
|
|
37
|
-
let skipUntilNextKey = false;
|
|
38
|
-
for (let i = 1; i < closingIndex; i++) {
|
|
39
|
-
const line = lines[i];
|
|
40
|
-
// Check if this is a top-level key (not indented, has colon)
|
|
41
|
-
const keyMatch = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:/);
|
|
42
|
-
if (keyMatch) {
|
|
43
|
-
const key = keyMatch[1];
|
|
44
|
-
if (FORBIDDEN_FRONTMATTER_KEYS.has(key)) {
|
|
45
|
-
stripped.push(key);
|
|
46
|
-
skipUntilNextKey = true;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
skipUntilNextKey = false;
|
|
50
|
-
}
|
|
51
|
-
// If we're skipping a forbidden key's multiline value (indented lines)
|
|
52
|
-
if (skipUntilNextKey) {
|
|
53
|
-
// Indented lines or continuation lines belong to the forbidden key
|
|
54
|
-
if (line.match(/^\s+/) || line.trim() === "") {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
// Non-indented non-empty line = new key
|
|
58
|
-
skipUntilNextKey = false;
|
|
59
|
-
}
|
|
60
|
-
filteredFrontmatterLines.push(line);
|
|
61
|
-
}
|
|
62
|
-
// Reconstruct the file
|
|
63
|
-
const before = lines[0]; // opening ---
|
|
64
|
-
const after = lines.slice(closingIndex); // closing --- + body
|
|
65
|
-
const result = [before, ...filteredFrontmatterLines, ...after].join("\n");
|
|
66
|
-
return { cleaned: result, stripped };
|
|
67
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Scaffold templates for Claude Code agents.
|
|
3
|
-
* Used by `web42 init` when platform is "claude".
|
|
4
|
-
*/
|
|
5
|
-
export function agentMdTemplate(name, description, skills) {
|
|
6
|
-
const skillsList = skills.length > 0
|
|
7
|
-
? skills.map((s) => ` - ${s}`).join("\n")
|
|
8
|
-
: " # No skills detected";
|
|
9
|
-
return `---
|
|
10
|
-
name: ${name}
|
|
11
|
-
description: ${description}
|
|
12
|
-
tools: Read, Grep, Glob, Bash
|
|
13
|
-
model: sonnet
|
|
14
|
-
skills:
|
|
15
|
-
${skillsList}
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
You are ${name}. ${description}
|
|
19
|
-
|
|
20
|
-
When invoked, analyze the task using your preloaded skills.
|
|
21
|
-
`;
|
|
22
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { InitConfig, InstalledAgent, InstallOptions, InstallResult, PackOptions, PackResult, PlatformAdapter, UninstallOptions, UninstallResult } from "../base.js";
|
|
2
|
-
export declare const HARDCODED_EXCLUDES: string[];
|
|
3
|
-
export declare class OpenClawAdapter implements PlatformAdapter {
|
|
4
|
-
name: string;
|
|
5
|
-
home: string;
|
|
6
|
-
extractInitConfig(cwd: string): InitConfig | null;
|
|
7
|
-
listInstalled(): Promise<InstalledAgent[]>;
|
|
8
|
-
uninstall(options: UninstallOptions): Promise<UninstallResult>;
|
|
9
|
-
pack(options: PackOptions): Promise<PackResult>;
|
|
10
|
-
install(options: InstallOptions): Promise<InstallResult>;
|
|
11
|
-
}
|
|
12
|
-
export declare const openclawAdapter: OpenClawAdapter;
|