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