@folpe/loom 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/agents/backend/AGENT.md +23 -3
- package/data/agents/database/AGENT.md +13 -4
- package/data/agents/frontend/AGENT.md +29 -5
- package/data/agents/marketing/AGENT.md +16 -2
- package/data/agents/orchestrator/AGENT.md +2 -2
- package/data/agents/performance/AGENT.md +9 -2
- package/data/agents/review-qa/AGENT.md +7 -3
- package/data/agents/security/AGENT.md +7 -4
- package/data/agents/tests/AGENT.md +9 -2
- package/data/agents/ux-ui/AGENT.md +11 -3
- package/data/presets/api-backend.yaml +2 -8
- package/data/presets/chrome-extension.yaml +2 -8
- package/data/presets/cli-tool.yaml +2 -7
- package/data/presets/e-commerce.yaml +2 -11
- package/data/presets/expo-mobile.yaml +2 -9
- package/data/presets/fullstack-auth.yaml +2 -11
- package/data/presets/landing-page.yaml +2 -8
- package/data/presets/mvp-lean.yaml +2 -9
- package/data/presets/saas-default.yaml +2 -10
- package/data/presets/saas-full.yaml +2 -15
- package/dist/index.js +539 -73
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -23,6 +23,34 @@ function listFiles(dir) {
|
|
|
23
23
|
if (!fs.existsSync(dir)) return [];
|
|
24
24
|
return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isFile()).map((d) => d.name).sort();
|
|
25
25
|
}
|
|
26
|
+
var TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
27
|
+
".md",
|
|
28
|
+
".ts",
|
|
29
|
+
".js",
|
|
30
|
+
".sh",
|
|
31
|
+
".dot",
|
|
32
|
+
".yaml",
|
|
33
|
+
".yml",
|
|
34
|
+
".json",
|
|
35
|
+
".css",
|
|
36
|
+
".html"
|
|
37
|
+
]);
|
|
38
|
+
function walkDir(dir, base = "") {
|
|
39
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
results.push(...walkDir(path.join(dir, entry.name), rel));
|
|
45
|
+
} else if (entry.isFile()) {
|
|
46
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
47
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
48
|
+
results.push(rel);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return results;
|
|
53
|
+
}
|
|
26
54
|
async function listAgents() {
|
|
27
55
|
const agentsDir = path.join(DATA_DIR, "agents");
|
|
28
56
|
const slugs = listSubDirs(agentsDir);
|
|
@@ -94,10 +122,16 @@ async function getAgent(slug) {
|
|
|
94
122
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
95
123
|
return { slug, rawContent: raw };
|
|
96
124
|
}
|
|
97
|
-
async function
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
125
|
+
async function getSkillWithFiles(slug) {
|
|
126
|
+
const skillDir = path.join(DATA_DIR, "skills", slug);
|
|
127
|
+
const mainPath = path.join(skillDir, "SKILL.md");
|
|
128
|
+
const mainContent = fs.readFileSync(mainPath, "utf-8");
|
|
129
|
+
const relativePaths = walkDir(skillDir);
|
|
130
|
+
const files = relativePaths.map((relativePath) => ({
|
|
131
|
+
relativePath,
|
|
132
|
+
content: fs.readFileSync(path.join(skillDir, relativePath), "utf-8")
|
|
133
|
+
}));
|
|
134
|
+
return { slug, mainContent, files };
|
|
101
135
|
}
|
|
102
136
|
async function getPreset(slug) {
|
|
103
137
|
const filePath = path.join(DATA_DIR, "presets", `${slug}.yaml`);
|
|
@@ -106,6 +140,116 @@ async function getPreset(slug) {
|
|
|
106
140
|
return { ...data, slug };
|
|
107
141
|
}
|
|
108
142
|
|
|
143
|
+
// src/lib/local-library.ts
|
|
144
|
+
import fs2 from "fs";
|
|
145
|
+
import path2 from "path";
|
|
146
|
+
import os from "os";
|
|
147
|
+
var LIBRARY_DIR = path2.join(os.homedir(), ".loom", "library");
|
|
148
|
+
function ensureDir(dir) {
|
|
149
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
function saveLocalAgent(slug, content) {
|
|
152
|
+
const dir = path2.join(LIBRARY_DIR, "agents", slug);
|
|
153
|
+
ensureDir(dir);
|
|
154
|
+
const filePath = path2.join(dir, "AGENT.md");
|
|
155
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
156
|
+
return filePath;
|
|
157
|
+
}
|
|
158
|
+
function saveLocalSkill(slug, files) {
|
|
159
|
+
const dir = path2.join(LIBRARY_DIR, "skills", slug);
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const filePath = path2.join(dir, file.relativePath);
|
|
162
|
+
ensureDir(path2.dirname(filePath));
|
|
163
|
+
fs2.writeFileSync(filePath, file.content, "utf-8");
|
|
164
|
+
}
|
|
165
|
+
return dir;
|
|
166
|
+
}
|
|
167
|
+
function saveLocalPreset(slug, content) {
|
|
168
|
+
const dir = path2.join(LIBRARY_DIR, "presets");
|
|
169
|
+
ensureDir(dir);
|
|
170
|
+
const filePath = path2.join(dir, `${slug}.yaml`);
|
|
171
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
172
|
+
return filePath;
|
|
173
|
+
}
|
|
174
|
+
function getLocalAgent(slug) {
|
|
175
|
+
const filePath = path2.join(LIBRARY_DIR, "agents", slug, "AGENT.md");
|
|
176
|
+
try {
|
|
177
|
+
const raw = fs2.readFileSync(filePath, "utf-8");
|
|
178
|
+
return { slug, rawContent: raw };
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
var TEXT_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
184
|
+
".md",
|
|
185
|
+
".ts",
|
|
186
|
+
".js",
|
|
187
|
+
".sh",
|
|
188
|
+
".dot",
|
|
189
|
+
".yaml",
|
|
190
|
+
".yml",
|
|
191
|
+
".json",
|
|
192
|
+
".css",
|
|
193
|
+
".html"
|
|
194
|
+
]);
|
|
195
|
+
function walkDir2(dir, base = "") {
|
|
196
|
+
let entries;
|
|
197
|
+
try {
|
|
198
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
199
|
+
} catch {
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
const results = [];
|
|
203
|
+
for (const entry of entries) {
|
|
204
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
205
|
+
if (entry.isDirectory()) {
|
|
206
|
+
results.push(...walkDir2(path2.join(dir, entry.name), rel));
|
|
207
|
+
} else if (entry.isFile()) {
|
|
208
|
+
const ext = path2.extname(entry.name).toLowerCase();
|
|
209
|
+
if (TEXT_EXTENSIONS2.has(ext)) {
|
|
210
|
+
results.push(rel);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return results;
|
|
215
|
+
}
|
|
216
|
+
function getLocalSkillWithFiles(slug) {
|
|
217
|
+
const dir = path2.join(LIBRARY_DIR, "skills", slug);
|
|
218
|
+
if (!fs2.existsSync(dir)) return null;
|
|
219
|
+
const relativePaths = walkDir2(dir);
|
|
220
|
+
if (relativePaths.length === 0) return null;
|
|
221
|
+
const files = relativePaths.map((relativePath) => ({
|
|
222
|
+
relativePath,
|
|
223
|
+
content: fs2.readFileSync(path2.join(dir, relativePath), "utf-8")
|
|
224
|
+
}));
|
|
225
|
+
return { slug, files };
|
|
226
|
+
}
|
|
227
|
+
function listSubDirs2(dir) {
|
|
228
|
+
try {
|
|
229
|
+
return fs2.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
230
|
+
} catch {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function listLocalResources() {
|
|
235
|
+
const items = [];
|
|
236
|
+
for (const slug of listSubDirs2(path2.join(LIBRARY_DIR, "agents"))) {
|
|
237
|
+
items.push({ slug, type: "agent" });
|
|
238
|
+
}
|
|
239
|
+
for (const slug of listSubDirs2(path2.join(LIBRARY_DIR, "skills"))) {
|
|
240
|
+
items.push({ slug, type: "skill" });
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const presetsDir = path2.join(LIBRARY_DIR, "presets");
|
|
244
|
+
const files = fs2.readdirSync(presetsDir).filter((f) => f.endsWith(".yaml"));
|
|
245
|
+
for (const f of files) {
|
|
246
|
+
items.push({ slug: f.replace(/\.yaml$/, ""), type: "preset" });
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
return items;
|
|
251
|
+
}
|
|
252
|
+
|
|
109
253
|
// src/commands/list.ts
|
|
110
254
|
function truncate(str, max) {
|
|
111
255
|
if (str.length <= max) return str;
|
|
@@ -116,6 +260,7 @@ function padEnd(str, len) {
|
|
|
116
260
|
}
|
|
117
261
|
async function listCommand(type) {
|
|
118
262
|
try {
|
|
263
|
+
const bundledSlugs = /* @__PURE__ */ new Set();
|
|
119
264
|
if (!type || type === "agents") {
|
|
120
265
|
const agents = await listAgents();
|
|
121
266
|
console.log(pc.bold(pc.cyan("\n Agents")));
|
|
@@ -124,6 +269,7 @@ async function listCommand(type) {
|
|
|
124
269
|
console.log(pc.dim(" No agents found."));
|
|
125
270
|
}
|
|
126
271
|
for (const a of agents) {
|
|
272
|
+
bundledSlugs.add(`agent:${a.slug}`);
|
|
127
273
|
console.log(
|
|
128
274
|
` ${padEnd(pc.green(a.slug), 30)} ${padEnd(a.name, 25)} ${pc.dim(truncate(a.description, 40))}`
|
|
129
275
|
);
|
|
@@ -137,6 +283,7 @@ async function listCommand(type) {
|
|
|
137
283
|
console.log(pc.dim(" No skills found."));
|
|
138
284
|
}
|
|
139
285
|
for (const s of skills) {
|
|
286
|
+
bundledSlugs.add(`skill:${s.slug}`);
|
|
140
287
|
console.log(
|
|
141
288
|
` ${padEnd(pc.green(s.slug), 30)} ${padEnd(s.name, 25)} ${pc.dim(truncate(s.description, 40))}`
|
|
142
289
|
);
|
|
@@ -150,12 +297,25 @@ async function listCommand(type) {
|
|
|
150
297
|
console.log(pc.dim(" No presets found."));
|
|
151
298
|
}
|
|
152
299
|
for (const p2 of presets) {
|
|
300
|
+
bundledSlugs.add(`preset:${p2.slug}`);
|
|
153
301
|
const meta = pc.dim(`(${p2.agentCount} agents, ${p2.skillCount} skills)`);
|
|
154
302
|
console.log(
|
|
155
303
|
` ${padEnd(pc.green(p2.slug), 30)} ${padEnd(p2.name, 25)} ${meta}`
|
|
156
304
|
);
|
|
157
305
|
}
|
|
158
306
|
}
|
|
307
|
+
const localItems = listLocalResources().filter(
|
|
308
|
+
(item) => !bundledSlugs.has(`${item.type}:${item.slug}`)
|
|
309
|
+
);
|
|
310
|
+
if (localItems.length > 0) {
|
|
311
|
+
console.log(pc.bold(pc.magenta("\n Installed (marketplace)")));
|
|
312
|
+
console.log(pc.dim(" " + "\u2500".repeat(60)));
|
|
313
|
+
for (const item of localItems) {
|
|
314
|
+
console.log(
|
|
315
|
+
` ${padEnd(pc.green(item.slug), 30)} ${pc.dim(`[${item.type}]`)}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
159
319
|
console.log();
|
|
160
320
|
} catch (error) {
|
|
161
321
|
handleError(error);
|
|
@@ -176,40 +336,41 @@ function handleError(error) {
|
|
|
176
336
|
import pc2 from "picocolors";
|
|
177
337
|
|
|
178
338
|
// src/lib/writer.ts
|
|
179
|
-
import
|
|
180
|
-
import
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
fs2.mkdirSync(dirPath, { recursive: true });
|
|
339
|
+
import fs3 from "fs";
|
|
340
|
+
import path3 from "path";
|
|
341
|
+
function ensureDir2(dirPath) {
|
|
342
|
+
fs3.mkdirSync(dirPath, { recursive: true });
|
|
184
343
|
}
|
|
185
|
-
function writeAgent(slug, content, cwd = process.cwd()) {
|
|
186
|
-
const dir =
|
|
187
|
-
|
|
188
|
-
const filePath =
|
|
189
|
-
|
|
344
|
+
function writeAgent(target, slug, content, cwd = process.cwd()) {
|
|
345
|
+
const dir = path3.join(cwd, target.dir, target.agentsSubdir, slug);
|
|
346
|
+
ensureDir2(dir);
|
|
347
|
+
const filePath = path3.join(dir, "AGENT.md");
|
|
348
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
190
349
|
return filePath;
|
|
191
350
|
}
|
|
192
|
-
function
|
|
193
|
-
const dir =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
351
|
+
function writeSkillDir(target, slug, files, cwd = process.cwd()) {
|
|
352
|
+
const dir = path3.join(cwd, target.dir, target.skillsSubdir, slug);
|
|
353
|
+
for (const file of files) {
|
|
354
|
+
const filePath = path3.join(dir, file.relativePath);
|
|
355
|
+
ensureDir2(path3.dirname(filePath));
|
|
356
|
+
fs3.writeFileSync(filePath, file.content, "utf-8");
|
|
357
|
+
}
|
|
358
|
+
return dir;
|
|
198
359
|
}
|
|
199
|
-
function writeOrchestrator(content, cwd = process.cwd()) {
|
|
200
|
-
const filePath =
|
|
201
|
-
|
|
202
|
-
|
|
360
|
+
function writeOrchestrator(target, content, cwd = process.cwd()) {
|
|
361
|
+
const filePath = path3.join(cwd, target.dir, target.orchestratorFile);
|
|
362
|
+
ensureDir2(path3.dirname(filePath));
|
|
363
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
203
364
|
return filePath;
|
|
204
365
|
}
|
|
205
|
-
function
|
|
206
|
-
const filePath =
|
|
207
|
-
|
|
366
|
+
function writeContextFile(target, content, cwd = process.cwd()) {
|
|
367
|
+
const filePath = path3.join(cwd, target.contextFile);
|
|
368
|
+
fs3.writeFileSync(filePath, content, "utf-8");
|
|
208
369
|
return filePath;
|
|
209
370
|
}
|
|
210
371
|
|
|
211
372
|
// src/commands/add.ts
|
|
212
|
-
async function addCommand(type, slug) {
|
|
373
|
+
async function addCommand(type, slug, target) {
|
|
213
374
|
if (type !== "agent" && type !== "skill") {
|
|
214
375
|
console.error(pc2.red(`
|
|
215
376
|
Error: Invalid type "${type}". Use "agent" or "skill".
|
|
@@ -219,25 +380,44 @@ async function addCommand(type, slug) {
|
|
|
219
380
|
try {
|
|
220
381
|
if (type === "agent") {
|
|
221
382
|
const agent = await getAgent(slug);
|
|
222
|
-
const filePath = writeAgent(slug, agent.rawContent);
|
|
383
|
+
const filePath = writeAgent(target, slug, agent.rawContent);
|
|
223
384
|
console.log(pc2.green(`
|
|
224
385
|
\u2713 Agent "${slug}" written to ${filePath}
|
|
225
386
|
`));
|
|
226
387
|
} else {
|
|
227
|
-
const skill = await
|
|
228
|
-
const
|
|
388
|
+
const skill = await getSkillWithFiles(slug);
|
|
389
|
+
const dirPath = writeSkillDir(target, slug, skill.files);
|
|
390
|
+
const fileCount = skill.files.length;
|
|
229
391
|
console.log(pc2.green(`
|
|
230
|
-
\u2713 Skill "${slug}" written to ${
|
|
392
|
+
\u2713 Skill "${slug}" written to ${dirPath} (${fileCount} file${fileCount !== 1 ? "s" : ""})
|
|
231
393
|
`));
|
|
232
394
|
}
|
|
233
|
-
} catch
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
395
|
+
} catch {
|
|
396
|
+
if (type === "agent") {
|
|
397
|
+
const local = getLocalAgent(slug);
|
|
398
|
+
if (local) {
|
|
399
|
+
const filePath = writeAgent(target, slug, local.rawContent);
|
|
400
|
+
console.log(pc2.green(`
|
|
401
|
+
\u2713 Agent "${slug}" written to ${filePath} ${pc2.dim("(from ~/.loom/library)")}
|
|
237
402
|
`));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
238
405
|
} else {
|
|
239
|
-
|
|
406
|
+
const local = getLocalSkillWithFiles(slug);
|
|
407
|
+
if (local) {
|
|
408
|
+
const dirPath = writeSkillDir(target, slug, local.files);
|
|
409
|
+
const fileCount = local.files.length;
|
|
410
|
+
console.log(pc2.green(`
|
|
411
|
+
\u2713 Skill "${slug}" written to ${dirPath} (${fileCount} file${fileCount !== 1 ? "s" : ""}) ${pc2.dim("(from ~/.loom/library)")}
|
|
412
|
+
`));
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
240
415
|
}
|
|
416
|
+
console.error(pc2.red(`
|
|
417
|
+
Error: ${type} "${slug}" not found.
|
|
418
|
+
`));
|
|
419
|
+
console.log(pc2.dim(` Try: loom marketplace search ${slug}
|
|
420
|
+
`));
|
|
241
421
|
process.exit(1);
|
|
242
422
|
}
|
|
243
423
|
}
|
|
@@ -249,28 +429,23 @@ import matter3 from "gray-matter";
|
|
|
249
429
|
|
|
250
430
|
// src/lib/generator.ts
|
|
251
431
|
import matter2 from "gray-matter";
|
|
252
|
-
function
|
|
432
|
+
function generateContextFile(preset, agents, target, skillSlugs = []) {
|
|
253
433
|
const lines = [];
|
|
254
434
|
lines.push(`# ${preset.name}`);
|
|
255
435
|
lines.push("");
|
|
256
|
-
lines.push(preset.
|
|
436
|
+
lines.push(preset.context.projectDescription.trim());
|
|
257
437
|
lines.push("");
|
|
258
438
|
if (preset.constitution.principles.length > 0) {
|
|
259
439
|
lines.push("## Principles");
|
|
440
|
+
lines.push("");
|
|
260
441
|
for (const p2 of preset.constitution.principles) {
|
|
261
442
|
lines.push(`- ${p2}`);
|
|
262
443
|
}
|
|
263
444
|
lines.push("");
|
|
264
445
|
}
|
|
265
|
-
if (preset.constitution.stack.length > 0) {
|
|
266
|
-
lines.push("## Stack");
|
|
267
|
-
for (const s of preset.constitution.stack) {
|
|
268
|
-
lines.push(`- ${s}`);
|
|
269
|
-
}
|
|
270
|
-
lines.push("");
|
|
271
|
-
}
|
|
272
446
|
if (preset.constitution.conventions.length > 0) {
|
|
273
447
|
lines.push("## Conventions");
|
|
448
|
+
lines.push("");
|
|
274
449
|
for (const c of preset.constitution.conventions) {
|
|
275
450
|
lines.push(`- ${c}`);
|
|
276
451
|
}
|
|
@@ -279,20 +454,52 @@ function generateClaudeMd(preset, agents) {
|
|
|
279
454
|
if (preset.constitution.customSections) {
|
|
280
455
|
for (const [title, content] of Object.entries(preset.constitution.customSections)) {
|
|
281
456
|
lines.push(`## ${title}`);
|
|
457
|
+
lines.push("");
|
|
282
458
|
lines.push(content);
|
|
283
459
|
lines.push("");
|
|
284
460
|
}
|
|
285
461
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
462
|
+
lines.push("## Commands");
|
|
463
|
+
lines.push("");
|
|
464
|
+
lines.push("```bash");
|
|
465
|
+
lines.push("npm run dev # Start development server");
|
|
466
|
+
lines.push("npm run build # Build for production");
|
|
467
|
+
lines.push("npm run lint # Run linter");
|
|
468
|
+
lines.push("npm test # Run tests");
|
|
469
|
+
lines.push("```");
|
|
470
|
+
lines.push("");
|
|
471
|
+
lines.push("<!-- loom:agents:start -->");
|
|
472
|
+
lines.push("## Agents");
|
|
473
|
+
lines.push("");
|
|
474
|
+
const nonOrchestrator = agents.filter((a) => a.slug !== "orchestrator");
|
|
475
|
+
if (nonOrchestrator.length > 0) {
|
|
476
|
+
lines.push(`This project uses ${nonOrchestrator.length} specialized agents coordinated by an orchestrator (\`${target.dir}/${target.orchestratorFile}\`).`);
|
|
477
|
+
lines.push("");
|
|
478
|
+
lines.push("| Agent | Role | Description |");
|
|
479
|
+
lines.push("|-------|------|-------------|");
|
|
480
|
+
for (const agent of nonOrchestrator) {
|
|
481
|
+
lines.push(`| \`${agent.slug}\` | ${agent.name} | ${agent.description} |`);
|
|
482
|
+
}
|
|
483
|
+
lines.push("");
|
|
484
|
+
}
|
|
485
|
+
lines.push("<!-- loom:agents:end -->");
|
|
486
|
+
lines.push("");
|
|
487
|
+
if (skillSlugs.length > 0) {
|
|
488
|
+
lines.push("<!-- loom:skills:start -->");
|
|
489
|
+
lines.push("## Skills");
|
|
490
|
+
lines.push("");
|
|
491
|
+
lines.push("Installed skills providing domain-specific conventions and patterns:");
|
|
492
|
+
lines.push("");
|
|
493
|
+
for (const slug of skillSlugs) {
|
|
494
|
+
lines.push(`- \`${slug}\``);
|
|
290
495
|
}
|
|
291
496
|
lines.push("");
|
|
497
|
+
lines.push("<!-- loom:skills:end -->");
|
|
498
|
+
lines.push("");
|
|
292
499
|
}
|
|
293
|
-
lines.push("##
|
|
500
|
+
lines.push("## How to use");
|
|
294
501
|
lines.push("");
|
|
295
|
-
lines.push(
|
|
502
|
+
lines.push(`The orchestrator agent (\`${target.dir}/${target.orchestratorFile}\`) is the main entry point. It analyzes tasks, breaks them into subtasks, and delegates to the appropriate specialized agents. Each agent has access to its assigned skills for domain-specific guidance.`);
|
|
296
503
|
lines.push("");
|
|
297
504
|
return lines.join("\n");
|
|
298
505
|
}
|
|
@@ -315,6 +522,81 @@ function generateOrchestrator(templateContent, agents, presetSkills) {
|
|
|
315
522
|
return matter2.stringify(newContent, newFrontmatter);
|
|
316
523
|
}
|
|
317
524
|
|
|
525
|
+
// src/lib/target.ts
|
|
526
|
+
var BUILTIN_TARGETS = {
|
|
527
|
+
"claude-code": {
|
|
528
|
+
name: "claude-code",
|
|
529
|
+
description: "Claude Code \u2014 .claude/ + CLAUDE.md",
|
|
530
|
+
dir: ".claude",
|
|
531
|
+
agentsSubdir: "agents",
|
|
532
|
+
skillsSubdir: "skills",
|
|
533
|
+
orchestratorFile: "orchestrator.md",
|
|
534
|
+
contextFile: "CLAUDE.md"
|
|
535
|
+
},
|
|
536
|
+
cursor: {
|
|
537
|
+
name: "cursor",
|
|
538
|
+
description: "Cursor \u2014 .cursor/ + .cursorrules",
|
|
539
|
+
dir: ".cursor",
|
|
540
|
+
agentsSubdir: "agents",
|
|
541
|
+
skillsSubdir: "skills",
|
|
542
|
+
orchestratorFile: "orchestrator.md",
|
|
543
|
+
contextFile: ".cursorrules"
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
var DEFAULT_TARGET = "claude-code";
|
|
547
|
+
function listTargetNames() {
|
|
548
|
+
return Object.keys(BUILTIN_TARGETS);
|
|
549
|
+
}
|
|
550
|
+
function resolveTarget(targetName, customDir, customContextFile) {
|
|
551
|
+
const builtin = BUILTIN_TARGETS[targetName];
|
|
552
|
+
if (builtin) return builtin;
|
|
553
|
+
if (targetName === "custom") {
|
|
554
|
+
if (!customDir || !customContextFile) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
'Target "custom" requires --target-dir and --context-file.'
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
name: "custom",
|
|
561
|
+
description: `Custom \u2014 ${customDir}/ + ${customContextFile}`,
|
|
562
|
+
dir: customDir,
|
|
563
|
+
agentsSubdir: "agents",
|
|
564
|
+
skillsSubdir: "skills",
|
|
565
|
+
orchestratorFile: "orchestrator.md",
|
|
566
|
+
contextFile: customContextFile
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
const available = [...listTargetNames(), "custom"].join(", ");
|
|
570
|
+
throw new Error(
|
|
571
|
+
`Unknown target "${targetName}". Available: ${available}.`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/lib/config.ts
|
|
576
|
+
import fs4 from "fs";
|
|
577
|
+
import path4 from "path";
|
|
578
|
+
var CONFIG_FILE = "loom.config.json";
|
|
579
|
+
function saveConfig(target, cwd = process.cwd()) {
|
|
580
|
+
const config = {
|
|
581
|
+
target: target.name,
|
|
582
|
+
targetDir: target.dir,
|
|
583
|
+
contextFile: target.contextFile
|
|
584
|
+
};
|
|
585
|
+
const filePath = path4.join(cwd, CONFIG_FILE);
|
|
586
|
+
fs4.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
587
|
+
}
|
|
588
|
+
function loadConfig(cwd = process.cwd()) {
|
|
589
|
+
const filePath = path4.join(cwd, CONFIG_FILE);
|
|
590
|
+
if (!fs4.existsSync(filePath)) return null;
|
|
591
|
+
try {
|
|
592
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
593
|
+
const config = JSON.parse(raw);
|
|
594
|
+
return resolveTarget(config.target, config.targetDir, config.contextFile);
|
|
595
|
+
} catch {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
318
600
|
// src/commands/init.ts
|
|
319
601
|
async function initCommand(presetSlug, opts = {}) {
|
|
320
602
|
try {
|
|
@@ -324,7 +606,7 @@ async function initCommand(presetSlug, opts = {}) {
|
|
|
324
606
|
process.exit(1);
|
|
325
607
|
}
|
|
326
608
|
if (!presetSlug && !hasFlags) {
|
|
327
|
-
await interactiveInit();
|
|
609
|
+
await interactiveInit(opts.target, opts.targetExplicit);
|
|
328
610
|
} else {
|
|
329
611
|
await nonInteractiveInit(presetSlug, opts);
|
|
330
612
|
}
|
|
@@ -339,8 +621,51 @@ async function initCommand(presetSlug, opts = {}) {
|
|
|
339
621
|
process.exit(1);
|
|
340
622
|
}
|
|
341
623
|
}
|
|
342
|
-
async function interactiveInit() {
|
|
624
|
+
async function interactiveInit(target, targetExplicit) {
|
|
343
625
|
p.intro(pc3.bgCyan(pc3.black(" loom init ")));
|
|
626
|
+
if (!targetExplicit) {
|
|
627
|
+
const builtinEntries = Object.values(BUILTIN_TARGETS);
|
|
628
|
+
const targetChoice = await p.select({
|
|
629
|
+
message: "Choose a target runtime",
|
|
630
|
+
options: [
|
|
631
|
+
...builtinEntries.map((t) => ({
|
|
632
|
+
value: t.name,
|
|
633
|
+
label: t.description
|
|
634
|
+
})),
|
|
635
|
+
{ value: "custom", label: "Custom \u2014 choose directory and context file" }
|
|
636
|
+
],
|
|
637
|
+
initialValue: target.name
|
|
638
|
+
});
|
|
639
|
+
if (p.isCancel(targetChoice)) {
|
|
640
|
+
p.cancel("Operation cancelled.");
|
|
641
|
+
process.exit(0);
|
|
642
|
+
}
|
|
643
|
+
if (targetChoice === "custom") {
|
|
644
|
+
const customDir = await p.text({
|
|
645
|
+
message: "Target directory",
|
|
646
|
+
placeholder: ".myruntime",
|
|
647
|
+
validate: (v) => !v || v.length === 0 ? "Required" : void 0
|
|
648
|
+
});
|
|
649
|
+
if (p.isCancel(customDir)) {
|
|
650
|
+
p.cancel("Operation cancelled.");
|
|
651
|
+
process.exit(0);
|
|
652
|
+
}
|
|
653
|
+
const customFile = await p.text({
|
|
654
|
+
message: "Context file name",
|
|
655
|
+
placeholder: "CONTEXT.md",
|
|
656
|
+
validate: (v) => !v || v.length === 0 ? "Required" : void 0
|
|
657
|
+
});
|
|
658
|
+
if (p.isCancel(customFile)) {
|
|
659
|
+
p.cancel("Operation cancelled.");
|
|
660
|
+
process.exit(0);
|
|
661
|
+
}
|
|
662
|
+
target = resolveTarget("custom", customDir, customFile);
|
|
663
|
+
} else {
|
|
664
|
+
target = BUILTIN_TARGETS[targetChoice];
|
|
665
|
+
}
|
|
666
|
+
} else {
|
|
667
|
+
p.log.info(`Target: ${target.description}`);
|
|
668
|
+
}
|
|
344
669
|
const presets = await listPresets();
|
|
345
670
|
if (presets.length === 0) {
|
|
346
671
|
p.cancel("No presets available.");
|
|
@@ -403,11 +728,13 @@ async function interactiveInit() {
|
|
|
403
728
|
}
|
|
404
729
|
const s = p.spinner();
|
|
405
730
|
s.start("Generating project files...");
|
|
406
|
-
await generateAndWrite(preset, agentSlugs, skillSlugs);
|
|
731
|
+
await generateAndWrite(preset, agentSlugs, skillSlugs, target);
|
|
732
|
+
saveConfig(target);
|
|
407
733
|
s.stop("Project files generated.");
|
|
408
|
-
p.outro(pc3.green(`Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s),
|
|
734
|
+
p.outro(pc3.green(`Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), ${target.contextFile} ready.`));
|
|
409
735
|
}
|
|
410
736
|
async function nonInteractiveInit(presetSlug, opts) {
|
|
737
|
+
const target = opts.target;
|
|
411
738
|
const preset = await getPreset(presetSlug);
|
|
412
739
|
const allAgents = await listAgents();
|
|
413
740
|
let agentSlugs = [...preset.agents];
|
|
@@ -448,23 +775,24 @@ async function nonInteractiveInit(presetSlug, opts) {
|
|
|
448
775
|
console.log(pc3.bold(pc3.cyan(`
|
|
449
776
|
Initializing preset "${preset.name}"...
|
|
450
777
|
`)));
|
|
451
|
-
await generateAndWrite(preset, agentSlugs, skillSlugs);
|
|
778
|
+
await generateAndWrite(preset, agentSlugs, skillSlugs, target);
|
|
779
|
+
saveConfig(target);
|
|
452
780
|
console.log(
|
|
453
781
|
pc3.bold(
|
|
454
782
|
pc3.cyan(
|
|
455
783
|
`
|
|
456
|
-
Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s),
|
|
784
|
+
Done! ${agentSlugs.length} agent(s), ${skillSlugs.length} skill(s), ${target.contextFile} ready.
|
|
457
785
|
`
|
|
458
786
|
)
|
|
459
787
|
)
|
|
460
788
|
);
|
|
461
789
|
}
|
|
462
|
-
async function generateAndWrite(preset, agentSlugs, skillSlugs) {
|
|
790
|
+
async function generateAndWrite(preset, agentSlugs, skillSlugs, target) {
|
|
463
791
|
const agentResults = await Promise.allSettled(
|
|
464
792
|
agentSlugs.map((slug) => getAgent(slug))
|
|
465
793
|
);
|
|
466
794
|
const skillResults = await Promise.allSettled(
|
|
467
|
-
skillSlugs.map((slug) =>
|
|
795
|
+
skillSlugs.map((slug) => getSkillWithFiles(slug))
|
|
468
796
|
);
|
|
469
797
|
const agentInfos = [];
|
|
470
798
|
const agentsWithSkills = [];
|
|
@@ -478,13 +806,14 @@ async function generateAndWrite(preset, agentSlugs, skillSlugs) {
|
|
|
478
806
|
if (slug === "orchestrator") {
|
|
479
807
|
orchestratorTemplate = result.value.rawContent;
|
|
480
808
|
} else {
|
|
481
|
-
writeAgent(slug, result.value.rawContent);
|
|
809
|
+
writeAgent(target, slug, result.value.rawContent);
|
|
482
810
|
console.log(pc3.green(` \u2713 Agent: ${slug}`));
|
|
483
811
|
}
|
|
484
812
|
agentInfos.push({
|
|
485
813
|
slug,
|
|
486
814
|
name: fm.name || slug,
|
|
487
|
-
role: fm.role || ""
|
|
815
|
+
role: fm.role || "",
|
|
816
|
+
description: fm.description || ""
|
|
488
817
|
});
|
|
489
818
|
agentsWithSkills.push({
|
|
490
819
|
slug,
|
|
@@ -502,22 +831,23 @@ async function generateAndWrite(preset, agentSlugs, skillSlugs) {
|
|
|
502
831
|
agentsWithSkills,
|
|
503
832
|
skillSlugs
|
|
504
833
|
);
|
|
505
|
-
writeOrchestrator(orchestratorContent);
|
|
506
|
-
console.log(pc3.green(` \u2713
|
|
834
|
+
writeOrchestrator(target, orchestratorContent);
|
|
835
|
+
console.log(pc3.green(` \u2713 ${target.orchestratorFile} generated`));
|
|
507
836
|
}
|
|
508
837
|
for (let i = 0; i < skillSlugs.length; i++) {
|
|
509
838
|
const slug = skillSlugs[i];
|
|
510
839
|
const result = skillResults[i];
|
|
511
840
|
if (result.status === "fulfilled") {
|
|
512
|
-
|
|
513
|
-
|
|
841
|
+
writeSkillDir(target, slug, result.value.files);
|
|
842
|
+
const fileCount = result.value.files.length;
|
|
843
|
+
console.log(pc3.green(` \u2713 Skill: ${slug} (${fileCount} file${fileCount !== 1 ? "s" : ""})`));
|
|
514
844
|
} else {
|
|
515
845
|
console.log(pc3.yellow(` \u26A0 Skill "${slug}" skipped: ${result.reason}`));
|
|
516
846
|
}
|
|
517
847
|
}
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
console.log(pc3.green(` \u2713
|
|
848
|
+
const contextContent = generateContextFile(preset, agentInfos, target, skillSlugs);
|
|
849
|
+
writeContextFile(target, contextContent);
|
|
850
|
+
console.log(pc3.green(` \u2713 ${target.contextFile} generated`));
|
|
521
851
|
}
|
|
522
852
|
function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillSlugs) {
|
|
523
853
|
const linkedToSelected = /* @__PURE__ */ new Set();
|
|
@@ -537,6 +867,106 @@ function computeAvailableSkills(preset, selectedAgentSlugs, allAgents, allSkillS
|
|
|
537
867
|
});
|
|
538
868
|
}
|
|
539
869
|
|
|
870
|
+
// src/commands/marketplace.ts
|
|
871
|
+
import pc4 from "picocolors";
|
|
872
|
+
var DEFAULT_API_URL = "https://loom.voidcorp.io";
|
|
873
|
+
function padEnd2(str, len) {
|
|
874
|
+
return str + " ".repeat(Math.max(0, len - str.length));
|
|
875
|
+
}
|
|
876
|
+
function truncate2(str, max) {
|
|
877
|
+
if (str.length <= max) return str;
|
|
878
|
+
return str.slice(0, max - 1) + "\u2026";
|
|
879
|
+
}
|
|
880
|
+
function getApiUrl() {
|
|
881
|
+
return process.env.LOOM_API_URL ?? DEFAULT_API_URL;
|
|
882
|
+
}
|
|
883
|
+
async function marketplaceSearchCommand(query, opts) {
|
|
884
|
+
try {
|
|
885
|
+
const url = new URL("/api/cli/marketplace", getApiUrl());
|
|
886
|
+
if (query) url.searchParams.set("q", query);
|
|
887
|
+
if (opts?.type) url.searchParams.set("type", opts.type);
|
|
888
|
+
if (opts?.sort) url.searchParams.set("sort", opts.sort);
|
|
889
|
+
const res = await fetch(url);
|
|
890
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
891
|
+
const data = await res.json();
|
|
892
|
+
if (data.items.length === 0) {
|
|
893
|
+
console.log(pc4.dim("\n No results found.\n"));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
console.log(
|
|
897
|
+
pc4.bold(pc4.cyan(`
|
|
898
|
+
Marketplace${query ? ` \u2014 "${query}"` : ""}`))
|
|
899
|
+
);
|
|
900
|
+
console.log(pc4.dim(" " + "\u2500".repeat(70)));
|
|
901
|
+
for (const item of data.items) {
|
|
902
|
+
const type = pc4.dim(`[${item.type}]`);
|
|
903
|
+
const installs = pc4.dim(`\u2193${item.installCount}`);
|
|
904
|
+
const author = item.authorName ? pc4.dim(`by ${item.authorName}`) : "";
|
|
905
|
+
console.log(
|
|
906
|
+
` ${padEnd2(pc4.green(item.slug), 25)} ${padEnd2(type, 14)} ${padEnd2(truncate2(item.title, 25), 27)} ${installs} ${author}`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
console.log(
|
|
910
|
+
pc4.dim(
|
|
911
|
+
`
|
|
912
|
+
Install with: ${pc4.reset("loom marketplace install <slug>")}
|
|
913
|
+
`
|
|
914
|
+
)
|
|
915
|
+
);
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (error instanceof Error) {
|
|
918
|
+
console.error(pc4.red(`
|
|
919
|
+
\u2717 ${error.message}
|
|
920
|
+
`));
|
|
921
|
+
} else {
|
|
922
|
+
console.error(pc4.red("\n \u2717 Could not reach the marketplace.\n"));
|
|
923
|
+
}
|
|
924
|
+
process.exit(1);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
async function marketplaceInstallCommand(slug) {
|
|
928
|
+
try {
|
|
929
|
+
const url = new URL("/api/cli/marketplace/install", getApiUrl());
|
|
930
|
+
const res = await fetch(url, {
|
|
931
|
+
method: "POST",
|
|
932
|
+
headers: { "Content-Type": "application/json" },
|
|
933
|
+
body: JSON.stringify({ slug })
|
|
934
|
+
});
|
|
935
|
+
if (!res.ok) {
|
|
936
|
+
const body = await res.json().catch(() => ({}));
|
|
937
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
938
|
+
}
|
|
939
|
+
const data = await res.json();
|
|
940
|
+
const r = data.resource;
|
|
941
|
+
if (r.type === "agent") {
|
|
942
|
+
saveLocalAgent(r.slug, r.content);
|
|
943
|
+
} else if (r.type === "skill") {
|
|
944
|
+
const files = r.files?.length ? r.files.map((f) => ({ relativePath: f.relativePath, content: f.content })) : [{ relativePath: "SKILL.md", content: r.content }];
|
|
945
|
+
saveLocalSkill(r.slug, files);
|
|
946
|
+
} else if (r.type === "preset") {
|
|
947
|
+
saveLocalPreset(r.slug, r.content);
|
|
948
|
+
}
|
|
949
|
+
console.log(
|
|
950
|
+
pc4.green(`
|
|
951
|
+
\u2713 Installed "${r.title}" (${r.type}) to ~/.loom/library/
|
|
952
|
+
`)
|
|
953
|
+
);
|
|
954
|
+
console.log(
|
|
955
|
+
pc4.dim(` Use it: loom add ${r.type} ${r.slug}
|
|
956
|
+
`)
|
|
957
|
+
);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
if (error instanceof Error) {
|
|
960
|
+
console.error(pc4.red(`
|
|
961
|
+
\u2717 ${error.message}
|
|
962
|
+
`));
|
|
963
|
+
} else {
|
|
964
|
+
console.error(pc4.red("\n \u2717 Installation failed.\n"));
|
|
965
|
+
}
|
|
966
|
+
process.exit(1);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
540
970
|
// src/index.ts
|
|
541
971
|
var require2 = createRequire(import.meta.url);
|
|
542
972
|
var { version } = require2("../package.json");
|
|
@@ -545,10 +975,46 @@ program.name("loom").description("Integrate Loom library (agents, skills, preset
|
|
|
545
975
|
program.command("list").description("List available agents, skills, and presets").argument("[type]", "Filter by type: agents, skills, or presets").action(async (type) => {
|
|
546
976
|
await listCommand(type);
|
|
547
977
|
});
|
|
548
|
-
program.command("add").description("Download an agent or skill from the library").argument("<type>", "Type: agent or skill").argument("<slug>", "Slug of the agent or skill").action(async (type, slug) => {
|
|
549
|
-
|
|
978
|
+
program.command("add").description("Download an agent or skill from the library").argument("<type>", "Type: agent or skill").argument("<slug>", "Slug of the agent or skill").option("--target <name>", `Output target: ${[...listTargetNames(), "custom"].join(", ")}`, DEFAULT_TARGET).option("--target-dir <dir>", "Custom target directory").option("--context-file <file>", "Custom context file name").action(async (type, slug, opts) => {
|
|
979
|
+
const savedConfig = loadConfig();
|
|
980
|
+
const target = opts.target !== DEFAULT_TARGET || opts.targetDir || opts.contextFile ? resolveTarget(opts.target, opts.targetDir, opts.contextFile) : savedConfig ?? BUILTIN_TARGETS[DEFAULT_TARGET];
|
|
981
|
+
await addCommand(type, slug, target);
|
|
550
982
|
});
|
|
551
|
-
program.command("init").description("Initialize a project with a preset (agents + skills +
|
|
552
|
-
|
|
983
|
+
program.command("init").description("Initialize a project with a preset (agents + skills + context file)").argument("[preset]", "Preset slug (interactive if omitted)").option("--add-agent <slugs...>", "Add extra agents").option("--remove-agent <slugs...>", "Remove agents from preset").option("--add-skill <slugs...>", "Add extra skills").option("--remove-skill <slugs...>", "Remove skills from preset").option("--claude", "Use Claude Code target (.claude/ + CLAUDE.md)").option("--cursor", "Use Cursor target (.cursor/ + .cursorrules)").option("--target <name>", `Output target: ${[...listTargetNames(), "custom"].join(", ")}`).option("--target-dir <dir>", "Custom target directory").option("--context-file <file>", "Custom context file name").action(async (preset, opts) => {
|
|
984
|
+
let target;
|
|
985
|
+
let targetExplicit = false;
|
|
986
|
+
if (opts.claude) {
|
|
987
|
+
target = BUILTIN_TARGETS["claude-code"];
|
|
988
|
+
targetExplicit = true;
|
|
989
|
+
} else if (opts.cursor) {
|
|
990
|
+
target = BUILTIN_TARGETS["cursor"];
|
|
991
|
+
targetExplicit = true;
|
|
992
|
+
} else if (opts.target) {
|
|
993
|
+
target = resolveTarget(
|
|
994
|
+
opts.target,
|
|
995
|
+
opts.targetDir,
|
|
996
|
+
opts.contextFile
|
|
997
|
+
);
|
|
998
|
+
targetExplicit = true;
|
|
999
|
+
} else {
|
|
1000
|
+
target = BUILTIN_TARGETS[DEFAULT_TARGET];
|
|
1001
|
+
}
|
|
1002
|
+
await initCommand(preset, {
|
|
1003
|
+
addAgent: opts.addAgent,
|
|
1004
|
+
removeAgent: opts.removeAgent,
|
|
1005
|
+
addSkill: opts.addSkill,
|
|
1006
|
+
removeSkill: opts.removeSkill,
|
|
1007
|
+
target,
|
|
1008
|
+
targetExplicit
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
var mp = program.command("marketplace").alias("mp").description("Browse and install community resources");
|
|
1012
|
+
mp.command("search").description("Search the marketplace").argument("[query]", "Search query").option("--type <type>", "Filter by type: agent, skill, preset").option("--sort <sort>", "Sort: popular, recent", "popular").action(
|
|
1013
|
+
async (query, opts) => {
|
|
1014
|
+
await marketplaceSearchCommand(query, opts);
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
mp.command("install").description("Install a resource from the marketplace").argument("<slug>", "Resource slug to install").action(async (slug) => {
|
|
1018
|
+
await marketplaceInstallCommand(slug);
|
|
553
1019
|
});
|
|
554
1020
|
program.parse();
|