@maskweaver/plugin 0.2.0 → 0.3.1
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/index.js +123 -234
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,9 +17,8 @@ function parseSimpleYaml(content) {
|
|
|
17
17
|
const line = lines[i];
|
|
18
18
|
const trimmed = line.trimStart();
|
|
19
19
|
if (!trimmed || trimmed.startsWith("#")) {
|
|
20
|
-
if (multilineKey)
|
|
20
|
+
if (multilineKey)
|
|
21
21
|
multilineValue.push("");
|
|
22
|
-
}
|
|
23
22
|
continue;
|
|
24
23
|
}
|
|
25
24
|
const indent = line.length - trimmed.length;
|
|
@@ -51,9 +50,8 @@ function parseSimpleYaml(content) {
|
|
|
51
50
|
const objKey = value.slice(0, colonIdx).trim();
|
|
52
51
|
const objVal = value.slice(colonIdx + 1).trim();
|
|
53
52
|
const arrayItem = {};
|
|
54
|
-
if (objVal)
|
|
53
|
+
if (objVal)
|
|
55
54
|
arrayItem[objKey] = parseValue(objVal);
|
|
56
|
-
}
|
|
57
55
|
let j = i + 1;
|
|
58
56
|
const itemIndent = indent + 2;
|
|
59
57
|
while (j < lines.length) {
|
|
@@ -64,16 +62,14 @@ function parseSimpleYaml(content) {
|
|
|
64
62
|
j++;
|
|
65
63
|
continue;
|
|
66
64
|
}
|
|
67
|
-
if (nextIndent < itemIndent || nextTrimmed.startsWith("- "))
|
|
65
|
+
if (nextIndent < itemIndent || nextTrimmed.startsWith("- "))
|
|
68
66
|
break;
|
|
69
|
-
}
|
|
70
67
|
if (nextTrimmed.includes(":")) {
|
|
71
68
|
const nColonIdx = nextTrimmed.indexOf(":");
|
|
72
69
|
const nKey = nextTrimmed.slice(0, nColonIdx).trim();
|
|
73
70
|
const nVal = nextTrimmed.slice(nColonIdx + 1).trim();
|
|
74
|
-
if (nVal)
|
|
71
|
+
if (nVal)
|
|
75
72
|
arrayItem[nKey] = parseValue(nVal);
|
|
76
|
-
}
|
|
77
73
|
}
|
|
78
74
|
j++;
|
|
79
75
|
}
|
|
@@ -110,8 +106,7 @@ function parseSimpleYaml(content) {
|
|
|
110
106
|
}
|
|
111
107
|
const colonIdx = trimmed.indexOf(":");
|
|
112
108
|
const key = trimmed.slice(0, colonIdx).trim();
|
|
113
|
-
const
|
|
114
|
-
const value = rawValue.trim();
|
|
109
|
+
const value = trimmed.slice(colonIdx + 1).trim();
|
|
115
110
|
const parent = stack[stack.length - 1];
|
|
116
111
|
if (!value) {
|
|
117
112
|
const nextLine = lines[i + 1];
|
|
@@ -169,21 +164,18 @@ class MaskLoader {
|
|
|
169
164
|
this.masksDir = masksDir;
|
|
170
165
|
}
|
|
171
166
|
async loadCatalog() {
|
|
172
|
-
if (this.catalog)
|
|
167
|
+
if (this.catalog)
|
|
173
168
|
return this.catalog;
|
|
174
|
-
}
|
|
175
169
|
const indexPath = path.join(this.masksDir, "index.json");
|
|
176
|
-
if (!fs.existsSync(indexPath))
|
|
177
|
-
throw new Error(`
|
|
178
|
-
}
|
|
170
|
+
if (!fs.existsSync(indexPath))
|
|
171
|
+
throw new Error(`Catalog not found: ${indexPath}`);
|
|
179
172
|
const content = fs.readFileSync(indexPath, "utf-8");
|
|
180
173
|
this.catalog = JSON.parse(content);
|
|
181
174
|
return this.catalog;
|
|
182
175
|
}
|
|
183
176
|
async load(maskId) {
|
|
184
|
-
if (this.cache.has(maskId))
|
|
177
|
+
if (this.cache.has(maskId))
|
|
185
178
|
return this.cache.get(maskId);
|
|
186
|
-
}
|
|
187
179
|
const catalog = await this.loadCatalog();
|
|
188
180
|
let entry = null;
|
|
189
181
|
let categoryId = null;
|
|
@@ -195,20 +187,14 @@ class MaskLoader {
|
|
|
195
187
|
break;
|
|
196
188
|
}
|
|
197
189
|
}
|
|
198
|
-
if (!entry || !categoryId)
|
|
190
|
+
if (!entry || !categoryId)
|
|
199
191
|
return null;
|
|
200
|
-
}
|
|
201
192
|
const filePath = path.join(this.masksDir, entry.file);
|
|
202
|
-
if (!fs.existsSync(filePath))
|
|
203
|
-
throw new Error(`
|
|
204
|
-
}
|
|
193
|
+
if (!fs.existsSync(filePath))
|
|
194
|
+
throw new Error(`File not found: ${filePath}`);
|
|
205
195
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
206
196
|
const parsed = filePath.endsWith(".yaml") || filePath.endsWith(".yml") ? parseSimpleYaml(content) : JSON.parse(content);
|
|
207
|
-
const loadedMask = {
|
|
208
|
-
...parsed,
|
|
209
|
-
category: categoryId,
|
|
210
|
-
filePath
|
|
211
|
-
};
|
|
197
|
+
const loadedMask = { ...parsed, category: categoryId, filePath };
|
|
212
198
|
this.cache.set(maskId, loadedMask);
|
|
213
199
|
return loadedMask;
|
|
214
200
|
}
|
|
@@ -217,10 +203,7 @@ class MaskLoader {
|
|
|
217
203
|
const result = [];
|
|
218
204
|
for (const [categoryId, category] of Object.entries(catalog.categories)) {
|
|
219
205
|
for (const mask of category.masks) {
|
|
220
|
-
result.push({
|
|
221
|
-
...mask,
|
|
222
|
-
category: categoryId
|
|
223
|
-
});
|
|
206
|
+
result.push({ ...mask, category: categoryId });
|
|
224
207
|
}
|
|
225
208
|
}
|
|
226
209
|
return result;
|
|
@@ -244,9 +227,8 @@ function buildRichPrompt(mask) {
|
|
|
244
227
|
parts.push(mask.profile.background.trim());
|
|
245
228
|
parts.push("");
|
|
246
229
|
parts.push("YOUR EXPERTISE:");
|
|
247
|
-
for (const exp of mask.profile.expertise)
|
|
230
|
+
for (const exp of mask.profile.expertise)
|
|
248
231
|
parts.push(`- ${exp}`);
|
|
249
|
-
}
|
|
250
232
|
parts.push("");
|
|
251
233
|
parts.push("YOUR THINKING STYLE:");
|
|
252
234
|
parts.push(mask.profile.thinkingStyle.trim());
|
|
@@ -261,84 +243,74 @@ function buildRichPrompt(mask) {
|
|
|
261
243
|
parts.push(`- Technical depth: ${style.technicalDepth}`);
|
|
262
244
|
parts.push("");
|
|
263
245
|
parts.push("YOUR STRENGTHS:");
|
|
264
|
-
for (const strength of mask.profile.strengths)
|
|
246
|
+
for (const strength of mask.profile.strengths)
|
|
265
247
|
parts.push(`- ${strength}`);
|
|
266
|
-
|
|
267
|
-
if (mask.profile.limitations && mask.profile.limitations.length > 0) {
|
|
248
|
+
if (mask.profile.limitations?.length) {
|
|
268
249
|
parts.push("");
|
|
269
250
|
parts.push("ACKNOWLEDGE YOUR LIMITATIONS:");
|
|
270
|
-
for (const limitation of mask.profile.limitations)
|
|
251
|
+
for (const limitation of mask.profile.limitations)
|
|
271
252
|
parts.push(`- ${limitation}`);
|
|
272
|
-
}
|
|
273
253
|
}
|
|
274
|
-
if (mask.behavior.signaturePhrases
|
|
254
|
+
if (mask.behavior.signaturePhrases?.length) {
|
|
275
255
|
parts.push("");
|
|
276
256
|
parts.push("PHRASES YOU MIGHT USE:");
|
|
277
|
-
for (const phrase of mask.behavior.signaturePhrases)
|
|
257
|
+
for (const phrase of mask.behavior.signaturePhrases)
|
|
278
258
|
parts.push(`- "${phrase}"`);
|
|
279
|
-
}
|
|
280
259
|
}
|
|
281
260
|
return parts.join(`
|
|
282
261
|
`);
|
|
283
262
|
}
|
|
284
|
-
var
|
|
285
|
-
|
|
263
|
+
var state = null;
|
|
264
|
+
var MaskweaverPlugin = async ({ client, directory }) => {
|
|
265
|
+
const masksDir = path.join(directory, ".opencode", "masks");
|
|
266
|
+
state = {
|
|
267
|
+
maskLoader: null,
|
|
268
|
+
activeMask: null,
|
|
269
|
+
masksDir
|
|
270
|
+
};
|
|
286
271
|
client.app.log({
|
|
287
272
|
service: "maskweaver",
|
|
288
273
|
level: "info",
|
|
289
|
-
message: "Maskweaver plugin loaded v0.
|
|
274
|
+
message: "Maskweaver plugin loaded v0.3.1"
|
|
290
275
|
});
|
|
291
|
-
|
|
292
|
-
maskLoader
|
|
293
|
-
activeMask: null,
|
|
294
|
-
masksDir: path.join(directory, ".opencode", "masks")
|
|
295
|
-
};
|
|
296
|
-
if (fs.existsSync(state.masksDir)) {
|
|
297
|
-
state.maskLoader = new MaskLoader(state.masksDir);
|
|
276
|
+
if (fs.existsSync(masksDir)) {
|
|
277
|
+
state.maskLoader = new MaskLoader(masksDir);
|
|
298
278
|
try {
|
|
299
279
|
await state.maskLoader.loadCatalog();
|
|
300
280
|
client.app.log({
|
|
301
281
|
service: "maskweaver",
|
|
302
282
|
level: "info",
|
|
303
|
-
message: `Masks
|
|
283
|
+
message: `Masks found at: ${masksDir}`
|
|
304
284
|
});
|
|
305
285
|
} catch (e) {
|
|
306
286
|
client.app.log({
|
|
307
287
|
service: "maskweaver",
|
|
308
288
|
level: "warn",
|
|
309
|
-
message: `Failed to load
|
|
289
|
+
message: `Failed to load masks: ${e}`
|
|
310
290
|
});
|
|
311
291
|
state.maskLoader = null;
|
|
312
292
|
}
|
|
313
|
-
} else {
|
|
314
|
-
client.app.log({
|
|
315
|
-
service: "maskweaver",
|
|
316
|
-
level: "info",
|
|
317
|
-
message: "No masks directory found, using empty catalog"
|
|
318
|
-
});
|
|
319
293
|
}
|
|
320
294
|
return {
|
|
321
|
-
|
|
322
|
-
{
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
295
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
296
|
+
if (state?.activeMask) {
|
|
297
|
+
const maskPrompt = `<ACTIVE_PERSONA>
|
|
298
|
+
You are currently embodying the "${state.activeMask.profile.name}" persona.
|
|
299
|
+
|
|
300
|
+
${buildRichPrompt(state.activeMask)}
|
|
301
|
+
</ACTIVE_PERSONA>`;
|
|
302
|
+
(output.system ||= []).push(maskPrompt);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
tool: {
|
|
306
|
+
list_masks: {
|
|
307
|
+
description: "List all available expert persona masks.",
|
|
308
|
+
args: {
|
|
309
|
+
category: { type: "string", description: "Filter by category" }
|
|
334
310
|
},
|
|
335
|
-
|
|
336
|
-
if (!state
|
|
337
|
-
return
|
|
338
|
-
success: false,
|
|
339
|
-
error: "No masks directory found",
|
|
340
|
-
hint: "Create a masks catalog at .opencode/masks/index.json"
|
|
341
|
-
};
|
|
311
|
+
async execute(args) {
|
|
312
|
+
if (!state?.maskLoader) {
|
|
313
|
+
return "Error: No masks directory found. Create .opencode/masks/index.json";
|
|
342
314
|
}
|
|
343
315
|
try {
|
|
344
316
|
const masks = await state.maskLoader.listAll();
|
|
@@ -347,198 +319,115 @@ var MaskweaverPlugin = async (ctx) => {
|
|
|
347
319
|
if (args.category) {
|
|
348
320
|
filtered = masks.filter((m) => m.category === args.category);
|
|
349
321
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
322
|
+
const lines = [];
|
|
323
|
+
lines.push(`Maskweaver v0.3.1 - ${filtered.length} masks available`);
|
|
324
|
+
lines.push(`Active mask: ${state.activeMask?.metadata.id || "none"}`);
|
|
325
|
+
lines.push("");
|
|
326
|
+
lines.push("Categories:");
|
|
327
|
+
for (const cat of categories) {
|
|
328
|
+
lines.push(` - ${cat.id}: ${cat.name} (${cat.count} masks)`);
|
|
329
|
+
}
|
|
330
|
+
lines.push("");
|
|
331
|
+
lines.push("Masks:");
|
|
332
|
+
for (const mask of filtered) {
|
|
333
|
+
lines.push(` - ${mask.id}: ${mask.name} [${mask.category}]`);
|
|
334
|
+
}
|
|
335
|
+
return lines.join(`
|
|
336
|
+
`);
|
|
362
337
|
} catch (e) {
|
|
363
|
-
return {
|
|
364
|
-
success: false,
|
|
365
|
-
error: `Failed to list masks: ${e}`
|
|
366
|
-
};
|
|
338
|
+
return `Error: ${e}`;
|
|
367
339
|
}
|
|
368
340
|
}
|
|
369
341
|
},
|
|
370
|
-
{
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
type: "object",
|
|
375
|
-
properties: {
|
|
376
|
-
maskId: {
|
|
377
|
-
type: "string",
|
|
378
|
-
description: 'The mask ID to select (e.g., "kent-beck", "linus-torvalds", "martin-fowler")'
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
required: ["maskId"]
|
|
342
|
+
select_mask: {
|
|
343
|
+
description: "Select and apply an expert persona mask.",
|
|
344
|
+
args: {
|
|
345
|
+
maskId: { type: "string", description: 'Mask ID (e.g., "kent-beck")' }
|
|
382
346
|
},
|
|
383
|
-
|
|
384
|
-
if (!state
|
|
385
|
-
return
|
|
386
|
-
success: false,
|
|
387
|
-
error: "No masks directory found",
|
|
388
|
-
hint: "Create a masks catalog at .opencode/masks/index.json"
|
|
389
|
-
};
|
|
347
|
+
async execute(args) {
|
|
348
|
+
if (!state?.maskLoader) {
|
|
349
|
+
return "Error: No masks directory found.";
|
|
390
350
|
}
|
|
391
351
|
try {
|
|
392
352
|
const mask = await state.maskLoader.load(args.maskId);
|
|
393
353
|
if (!mask) {
|
|
394
354
|
const available = await state.maskLoader.listAll();
|
|
395
|
-
return {
|
|
396
|
-
|
|
397
|
-
error: `Mask not found: ${args.maskId}`,
|
|
398
|
-
availableMasks: available.map((m) => m.id)
|
|
399
|
-
};
|
|
355
|
+
return `Error: Mask "${args.maskId}" not found.
|
|
356
|
+
Available: ${available.map((m) => m.id).join(", ")}`;
|
|
400
357
|
}
|
|
401
358
|
state.activeMask = mask;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
mask: {
|
|
410
|
-
id: mask.metadata.id,
|
|
411
|
-
name: mask.profile.name,
|
|
412
|
-
tagline: mask.profile.tagline,
|
|
413
|
-
category: mask.category,
|
|
414
|
-
expertise: mask.profile.expertise
|
|
415
|
-
},
|
|
416
|
-
systemPromptPreview: mask.behavior.systemPrompt.slice(0, 200) + "...",
|
|
417
|
-
message: `Now embodying: ${mask.profile.name} - ${mask.profile.tagline}`
|
|
418
|
-
};
|
|
359
|
+
return `✓ Mask activated: ${mask.profile.name}
|
|
360
|
+
|
|
361
|
+
"${mask.profile.tagline}"
|
|
362
|
+
|
|
363
|
+
Expertise: ${mask.profile.expertise.join(", ")}
|
|
364
|
+
|
|
365
|
+
The mask prompt will be injected into all future messages.`;
|
|
419
366
|
} catch (e) {
|
|
420
|
-
return {
|
|
421
|
-
success: false,
|
|
422
|
-
error: `Failed to select mask: ${e}`
|
|
423
|
-
};
|
|
367
|
+
return `Error: ${e}`;
|
|
424
368
|
}
|
|
425
369
|
}
|
|
426
370
|
},
|
|
427
|
-
{
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
state.activeMask = null;
|
|
438
|
-
if (previousMask) {
|
|
439
|
-
client.app.log({
|
|
440
|
-
service: "maskweaver",
|
|
441
|
-
level: "info",
|
|
442
|
-
message: `Mask deselected: ${previousMask.profile.name}`
|
|
443
|
-
});
|
|
444
|
-
return {
|
|
445
|
-
success: true,
|
|
446
|
-
previousMask: previousMask.metadata.id,
|
|
447
|
-
message: "Returned to default behavior"
|
|
448
|
-
};
|
|
371
|
+
deselect_mask: {
|
|
372
|
+
description: "Remove the current mask and return to default behavior.",
|
|
373
|
+
args: {},
|
|
374
|
+
async execute() {
|
|
375
|
+
const prev = state?.activeMask;
|
|
376
|
+
if (state)
|
|
377
|
+
state.activeMask = null;
|
|
378
|
+
if (prev) {
|
|
379
|
+
return `✓ Mask removed: ${prev.profile.name}
|
|
380
|
+
Returned to default behavior.`;
|
|
449
381
|
}
|
|
450
|
-
return
|
|
451
|
-
success: true,
|
|
452
|
-
message: "No mask was active"
|
|
453
|
-
};
|
|
382
|
+
return "No mask was active.";
|
|
454
383
|
}
|
|
455
384
|
},
|
|
456
|
-
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
type: "object",
|
|
461
|
-
properties: {
|
|
462
|
-
maskId: {
|
|
463
|
-
type: "string",
|
|
464
|
-
description: "The mask ID to get prompt for. If not provided, uses the active mask."
|
|
465
|
-
}
|
|
466
|
-
},
|
|
467
|
-
required: []
|
|
385
|
+
get_mask_prompt: {
|
|
386
|
+
description: "View the full system prompt for a mask.",
|
|
387
|
+
args: {
|
|
388
|
+
maskId: { type: "string", description: "Mask ID (uses active mask if not specified)" }
|
|
468
389
|
},
|
|
469
|
-
|
|
470
|
-
if (!state
|
|
471
|
-
return
|
|
472
|
-
success: false,
|
|
473
|
-
error: "No masks directory found"
|
|
474
|
-
};
|
|
475
|
-
}
|
|
390
|
+
async execute(args) {
|
|
391
|
+
if (!state?.maskLoader)
|
|
392
|
+
return "Error: No masks directory.";
|
|
476
393
|
const maskId = args.maskId || state.activeMask?.metadata.id;
|
|
477
|
-
if (!maskId)
|
|
478
|
-
return
|
|
479
|
-
success: false,
|
|
480
|
-
error: "No mask specified and no active mask"
|
|
481
|
-
};
|
|
482
|
-
}
|
|
394
|
+
if (!maskId)
|
|
395
|
+
return "Error: No mask specified and no active mask.";
|
|
483
396
|
try {
|
|
484
397
|
const mask = await state.maskLoader.load(maskId);
|
|
485
|
-
if (!mask)
|
|
486
|
-
return {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
return {
|
|
492
|
-
success: true,
|
|
493
|
-
maskId: mask.metadata.id,
|
|
494
|
-
name: mask.profile.name,
|
|
495
|
-
systemPrompt: buildRichPrompt(mask)
|
|
496
|
-
};
|
|
398
|
+
if (!mask)
|
|
399
|
+
return `Error: Mask "${maskId}" not found.`;
|
|
400
|
+
return `# ${mask.profile.name}
|
|
401
|
+
|
|
402
|
+
${buildRichPrompt(mask)}`;
|
|
497
403
|
} catch (e) {
|
|
498
|
-
return {
|
|
499
|
-
success: false,
|
|
500
|
-
error: `Failed to get mask prompt: ${e}`
|
|
501
|
-
};
|
|
404
|
+
return `Error: ${e}`;
|
|
502
405
|
}
|
|
503
406
|
}
|
|
504
407
|
},
|
|
505
|
-
{
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
type: "object",
|
|
510
|
-
properties: {},
|
|
511
|
-
required: []
|
|
512
|
-
},
|
|
513
|
-
execute: async () => {
|
|
408
|
+
maskweaver_status: {
|
|
409
|
+
description: "Check Maskweaver status.",
|
|
410
|
+
args: {},
|
|
411
|
+
async execute() {
|
|
514
412
|
let masksCount = 0;
|
|
515
413
|
let categoriesCount = 0;
|
|
516
|
-
if (state
|
|
414
|
+
if (state?.maskLoader) {
|
|
517
415
|
try {
|
|
518
416
|
const masks = await state.maskLoader.listAll();
|
|
519
417
|
const categories = await state.maskLoader.listCategories();
|
|
520
418
|
masksCount = masks.length;
|
|
521
419
|
categoriesCount = categories.length;
|
|
522
|
-
} catch (
|
|
420
|
+
} catch (_e) {}
|
|
523
421
|
}
|
|
524
|
-
return
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
id: state.activeMask.metadata.id,
|
|
531
|
-
name: state.activeMask.profile.name,
|
|
532
|
-
category: state.activeMask.category
|
|
533
|
-
} : null,
|
|
534
|
-
stats: {
|
|
535
|
-
masks: masksCount,
|
|
536
|
-
categories: categoriesCount
|
|
537
|
-
}
|
|
538
|
-
};
|
|
422
|
+
return `Maskweaver v0.3.1
|
|
423
|
+
Masks directory: ${state?.masksDir}
|
|
424
|
+
Available: ${state?.maskLoader ? "yes" : "no"}
|
|
425
|
+
Total masks: ${masksCount}
|
|
426
|
+
Categories: ${categoriesCount}
|
|
427
|
+
Active mask: ${state?.activeMask ? `${state.activeMask.profile.name} (${state.activeMask.metadata.id})` : "none"}`;
|
|
539
428
|
}
|
|
540
429
|
}
|
|
541
|
-
|
|
430
|
+
}
|
|
542
431
|
};
|
|
543
432
|
};
|
|
544
433
|
var src_default = MaskweaverPlugin;
|