@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.
Files changed (2) hide show
  1. package/dist/index.js +123 -234
  2. 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 rawValue = trimmed.slice(colonIdx + 1);
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(`Mask catalog not found: ${indexPath}`);
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(`Mask file not found: ${filePath}`);
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 && mask.behavior.signaturePhrases.length > 0) {
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 MaskweaverPlugin = async (ctx) => {
285
- const { directory, client } = ctx;
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.2.0"
274
+ message: "Maskweaver plugin loaded v0.3.1"
290
275
  });
291
- const state = {
292
- maskLoader: null,
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 directory found: ${state.masksDir}`
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 mask catalog: ${e}`
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
- tools: [
322
- {
323
- name: "list_masks",
324
- description: "List all available expert persona masks. Returns mask IDs, names, categories, and tags.",
325
- parameters: {
326
- type: "object",
327
- properties: {
328
- category: {
329
- type: "string",
330
- description: 'Optional: filter by category (e.g., "software-engineering", "ai-ml", "architecture")'
331
- }
332
- },
333
- required: []
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
- execute: async (args) => {
336
- if (!state.maskLoader) {
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
- return {
351
- success: true,
352
- activeMask: state.activeMask?.metadata.id || null,
353
- categories,
354
- masks: filtered.map((m) => ({
355
- id: m.id,
356
- name: m.name,
357
- category: m.category,
358
- tags: m.tags
359
- })),
360
- total: filtered.length
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
- name: "select_mask",
372
- description: "Select and apply an expert persona mask. This changes the AI behavior to match the selected expert.",
373
- parameters: {
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
- execute: async (args) => {
384
- if (!state.maskLoader) {
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
- success: false,
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
- client.app.log({
403
- service: "maskweaver",
404
- level: "info",
405
- message: `Mask selected: ${mask.profile.name}`
406
- });
407
- return {
408
- success: true,
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
- name: "deselect_mask",
429
- description: "Deselect the current mask and return to default AI behavior.",
430
- parameters: {
431
- type: "object",
432
- properties: {},
433
- required: []
434
- },
435
- execute: async () => {
436
- const previousMask = state.activeMask;
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
- name: "get_mask_prompt",
458
- description: "Get the full system prompt for a mask. Useful for understanding what a mask does.",
459
- parameters: {
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
- execute: async (args) => {
470
- if (!state.maskLoader) {
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
- success: false,
488
- error: `Mask not found: ${maskId}`
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
- name: "maskweaver_status",
507
- description: "Get the current Maskweaver status including active mask and available masks count.",
508
- parameters: {
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.maskLoader) {
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 (e) {}
420
+ } catch (_e) {}
523
421
  }
524
- return {
525
- success: true,
526
- version: "0.2.0",
527
- masksDir: state.masksDir,
528
- masksAvailable: state.maskLoader !== null,
529
- activeMask: state.activeMask ? {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maskweaver/plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Maskweaver plugin for opencode - Expert AI personas",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",