@mistralys/persona-builder 2.1.2 → 2.2.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/README.md CHANGED
@@ -6,7 +6,8 @@ Define your personas once as simple YAML + Markdown sources, and the library gen
6
6
 
7
7
  ## ✨ Features
8
8
 
9
- - **Dual-target output** — generates both `.agent.md` (VS Code) and `.md` (Claude Code) from a single source
9
+ - **Multi-target output** — generates VS Code `.agent.md`, Claude Code `.md`, Deep Agents `.md`, and any custom format from a single source
10
+ - **Extensible target registry** — register custom targets via `TargetRegistry` without touching core code; each target declares its own output key, frontmatter template, and context flags
10
11
  - **YAML + Markdown templating** — separate metadata from content; merge them at build time with `{{variables}}`, `{{> partials}}`, and `{{#if}}` conditionals
11
12
  - **Shared + per-suite partials** — reuse content fragments across personas with local overrides
12
13
  - **Plugin architecture** — hook into context building, post-rendering, validation, and frontmatter generation
@@ -34,8 +35,10 @@ const summary = await build({
34
35
  suites: {
35
36
  'my-suite': {
36
37
  srcDir: path.resolve('./personas/my-suite'),
37
- outVscode: path.resolve('./dist/vscode'),
38
- outClaudeCode: path.resolve('./dist/claude-code'),
38
+ outputDirs: {
39
+ vscode: path.resolve('./dist/vscode'),
40
+ 'claude-code': path.resolve('./dist/claude-code'),
41
+ },
39
42
  },
40
43
  },
41
44
  sharedPartialsDir: path.resolve('./personas/shared/partials'),
@@ -75,6 +78,7 @@ See the [CLI docs](docs/cli.md) for config file format and all flags.
75
78
  | [Configuration Reference](docs/configuration.md) | `BuildConfig`, `SuiteConfig`, and `BuildSummary` fields |
76
79
  | [CLI Reference](docs/cli.md) | Command-line flags, config file format, and common patterns |
77
80
  | [Public API](docs/api.md) | All exported types and functions |
81
+ | [Project Manifest](docs/agents/project-manifest/README.md) | Canonical documentation for AI agent sessions |
78
82
 
79
83
  ## 🔌 Plugins
80
84
 
@@ -91,3 +95,13 @@ the `PersonaBuildPlugin` interface, examples, and the available hooks.
91
95
  ## 📄 License
92
96
 
93
97
  MIT
98
+
99
+ ---
100
+
101
+ ## Release Workflow
102
+
103
+ 1. Add changelog entries
104
+ 2. `npm version 0.0.0` - Updates package and lock versions + commit
105
+ 3. `npm publish` - Publish version on NPM
106
+ 4. `git push origin 0.0.0` - Add the tag in GIT
107
+ 5. Add the release on Github
package/dist/cli.cjs CHANGED
@@ -28,18 +28,27 @@ function resolvePartials(text, partialsMap, depth = 0) {
28
28
 
29
29
  // src/engine/conditionals.ts
30
30
  function resolveConditionals(text, context) {
31
- return text.replace(
32
- /\n*\{\{#if (\w+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}\n*/g,
33
- (_match, flag, inner, elseInner) => {
34
- if (context[flag]) {
35
- return "\n" + inner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
36
- }
37
- if (elseInner !== void 0) {
38
- return "\n" + elseInner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
39
- }
40
- return "\n";
41
- }
31
+ const noNestedIf = String.raw`(?:(?!\{\{#if\b)[\s\S])*?`;
32
+ const pattern = new RegExp(
33
+ String.raw`\n*\{\{#if (\w+)\}\}(${noNestedIf})` + String.raw`(?:\{\{else\}\}(${noNestedIf}))?\{\{\/if\}\}\n*`,
34
+ "g"
42
35
  );
36
+ const resolve = (_match, flag, inner, elseInner) => {
37
+ if (context[flag]) {
38
+ return "\n" + inner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
39
+ }
40
+ if (elseInner !== void 0) {
41
+ return "\n" + elseInner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
42
+ }
43
+ return "\n";
44
+ };
45
+ let result = text;
46
+ let prev;
47
+ do {
48
+ prev = result;
49
+ result = result.replace(pattern, resolve);
50
+ } while (result !== prev);
51
+ return result;
43
52
  }
44
53
 
45
54
  // src/engine/variables.ts
@@ -98,11 +107,11 @@ function runSuiteInit(plugins, suite, sharedMeta) {
98
107
  }
99
108
  }
100
109
  }
101
- function runBuildContext(plugins, ctx, persona, suite) {
110
+ function runBuildContext(plugins, ctx, persona, suite, target) {
102
111
  let accumulated = ctx;
103
112
  for (const plugin of plugins) {
104
113
  if (typeof plugin.onBuildContext === "function") {
105
- accumulated = plugin.onBuildContext(accumulated, persona, suite);
114
+ accumulated = plugin.onBuildContext(accumulated, persona, suite, target);
106
115
  }
107
116
  }
108
117
  return accumulated;
@@ -127,7 +136,10 @@ function runValidate(plugins, persona, suite, target) {
127
136
  return results;
128
137
  }
129
138
 
130
- // src/builders/frontmatter.ts
139
+ // src/targets/types.ts
140
+ var TARGET_VSCODE = "vscode";
141
+ var TARGET_CLAUDE_CODE = "claude-code";
142
+ var TARGET_DEEP_AGENTS = "deep-agents";
131
143
  var DEFAULT_FRONTMATTER_VSCODE = `---
132
144
  name: '{{name}} v{{version}}'
133
145
  description: '{{description}}'
@@ -140,7 +152,13 @@ model: {{cc_model}}
140
152
  memory: {{cc_memory}}
141
153
  allowedTools: [{{cc_tools_list}}]
142
154
  ---`;
143
- function resolveFrontmatterTemplate(target, plugins, configTemplates) {
155
+ var DEFAULT_FRONTMATTER_DEEP_AGENTS = `---
156
+ name: {{name}}
157
+ description: {{description}}
158
+ ---`;
159
+
160
+ // src/builders/frontmatter.ts
161
+ function resolveFrontmatterTemplate(target, plugins, configTemplates, registry) {
144
162
  for (const plugin of plugins) {
145
163
  if (plugin.frontmatterTemplates && target in plugin.frontmatterTemplates) {
146
164
  const tpl = plugin.frontmatterTemplates[target];
@@ -151,7 +169,10 @@ function resolveFrontmatterTemplate(target, plugins, configTemplates) {
151
169
  const tpl = configTemplates[target];
152
170
  if (tpl !== void 0) return tpl;
153
171
  }
154
- return target === "vscode" ? DEFAULT_FRONTMATTER_VSCODE : DEFAULT_FRONTMATTER_CLAUDE_CODE;
172
+ if (registry && registry.has(target)) {
173
+ return registry.get(target).defaultFrontmatter;
174
+ }
175
+ return DEFAULT_FRONTMATTER_VSCODE;
155
176
  }
156
177
  function renderFrontmatter(template, context, filename) {
157
178
  let rendered = resolveConditionals(template, context);
@@ -159,6 +180,112 @@ function renderFrontmatter(template, context, filename) {
159
180
  return rendered;
160
181
  }
161
182
 
183
+ // src/targets/registry.ts
184
+ var TargetRegistry = class _TargetRegistry {
185
+ // Map preserves insertion order — names() and allDefinitions() are
186
+ // therefore deterministic and match registration sequence. This is
187
+ // intentional: the built-in registry guarantees ['vscode', 'claude-code']
188
+ // ordering for the default targets (AC-2).
189
+ _definitions = /* @__PURE__ */ new Map();
190
+ /**
191
+ * Register a new target definition.
192
+ *
193
+ * @param definition The target descriptor to register.
194
+ * @throws {Error} If a target with the same `name` is already registered.
195
+ */
196
+ register(definition) {
197
+ if (this._definitions.has(definition.name)) {
198
+ throw new Error(
199
+ `TargetRegistry: target "${definition.name}" is already registered. Use a unique name or remove the existing registration first.`
200
+ );
201
+ }
202
+ this._definitions.set(definition.name, definition);
203
+ }
204
+ /**
205
+ * Retrieve a registered target definition by name.
206
+ *
207
+ * Returns a shallow copy — mutating the returned object does not affect
208
+ * the registry's internal state.
209
+ *
210
+ * @param name The target name to look up.
211
+ * @returns A shallow copy of the matching TargetDefinition.
212
+ * @throws {Error} If no target with the given name is registered.
213
+ */
214
+ get(name) {
215
+ const def = this._definitions.get(name);
216
+ if (!def) {
217
+ const known = this.names().join(", ") || "(none)";
218
+ throw new Error(
219
+ `TargetRegistry: target "${name}" is not registered. Registered targets: ${known}.`
220
+ );
221
+ }
222
+ return { ...def };
223
+ }
224
+ /**
225
+ * Returns `true` if a target with the given name is registered.
226
+ *
227
+ * @param name The target name to check.
228
+ */
229
+ has(name) {
230
+ return this._definitions.has(name);
231
+ }
232
+ /**
233
+ * Returns the names of all registered targets, in registration order.
234
+ */
235
+ names() {
236
+ return Array.from(this._definitions.keys());
237
+ }
238
+ /**
239
+ * Returns all registered TargetDefinition objects, in registration order.
240
+ *
241
+ * Returns shallow copies — mutating a returned definition does not affect
242
+ * the registry's internal state.
243
+ */
244
+ allDefinitions() {
245
+ return Array.from(this._definitions.values()).map((def) => ({ ...def }));
246
+ }
247
+ /**
248
+ * Returns a new TargetRegistry pre-populated with the same definitions.
249
+ *
250
+ * Useful for test isolation: clone the `defaultRegistry` to get an
251
+ * independent copy that can be mutated without affecting the singleton.
252
+ */
253
+ clone() {
254
+ const copy = new _TargetRegistry();
255
+ for (const def of this._definitions.values()) {
256
+ copy.register({ ...def });
257
+ }
258
+ return copy;
259
+ }
260
+ };
261
+
262
+ // src/targets/built-in.ts
263
+ var defaultRegistry = new TargetRegistry();
264
+ defaultRegistry.register({
265
+ name: TARGET_VSCODE,
266
+ outputDirKey: "vscode",
267
+ filenameContextKey: "vs_file_name",
268
+ defaultFrontmatter: DEFAULT_FRONTMATTER_VSCODE,
269
+ contextFlags: { target_vscode: true },
270
+ defaultEnabled: true
271
+ });
272
+ defaultRegistry.register({
273
+ name: TARGET_CLAUDE_CODE,
274
+ outputDirKey: "claude-code",
275
+ filenameContextKey: "cc_file_name",
276
+ defaultFrontmatter: DEFAULT_FRONTMATTER_CLAUDE_CODE,
277
+ contextFlags: { target_claude_code: true },
278
+ defaultEnabled: true
279
+ });
280
+ defaultRegistry.register({
281
+ name: TARGET_DEEP_AGENTS,
282
+ outputDirKey: "deep-agents",
283
+ filenameContextKey: "da_file_name",
284
+ defaultFrontmatter: DEFAULT_FRONTMATTER_DEEP_AGENTS,
285
+ contextFlags: { target_deep_agents: true },
286
+ defaultEnabled: false
287
+ });
288
+
162
289
  // src/builders/persona-builder.ts
163
290
  async function discoverSuitePersonaYamls(suiteConfig) {
164
291
  const metaSubdir = suiteConfig.metaSubdir ?? "meta";
@@ -186,6 +313,18 @@ async function loadPersonaYaml(yamlPath) {
186
313
  }
187
314
  return record;
188
315
  }
316
+ function resolveOutputDir(target, suiteConfig, definition) {
317
+ const merged = {};
318
+ if (suiteConfig.outVscode) merged["vscode"] = suiteConfig.outVscode;
319
+ if (suiteConfig.outClaudeCode) merged["claude-code"] = suiteConfig.outClaudeCode;
320
+ if (suiteConfig.outputDirs) Object.assign(merged, suiteConfig.outputDirs);
321
+ const lookupKey = definition?.outputDirKey ?? target;
322
+ const dir = merged[lookupKey];
323
+ if (dir) return dir;
324
+ throw new Error(
325
+ `buildPersona: no output directory configured for target "${target}". Add outputDirs['${lookupKey}'] to the suite config, or, for the built-in targets, provide the outVscode / outClaudeCode fields.`
326
+ );
327
+ }
189
328
  async function buildAgentNameMap(config) {
190
329
  const agentMap = {};
191
330
  for (const [, suiteConfig] of Object.entries(config.suites)) {
@@ -199,13 +338,16 @@ async function buildAgentNameMap(config) {
199
338
  const slug = typeof persona["slug"] === "string" ? persona["slug"] : path2__default.default.basename(yamlPath, ".yaml");
200
339
  const name = typeof persona["name"] === "string" ? persona["name"] : slug;
201
340
  const version = typeof persona["version"] === "string" ? persona["version"] : defaultVersion;
202
- const key = `agent_${slug.replace(/-/g, "_")}`;
341
+ const underscoredSlug = slug.replace(/-/g, "_");
342
+ const key = `agent_${underscoredSlug}`;
203
343
  agentMap[key] = `${name} v${version}`;
344
+ const slugKey = `agent_slug_${underscoredSlug}`;
345
+ agentMap[slugKey] = slug;
204
346
  }
205
347
  }
206
348
  return agentMap;
207
349
  }
208
- function buildContext(personaMeta, sharedMeta, agentMap = {}) {
350
+ function buildContext(personaMeta, sharedMeta, agentMap = {}, target, registry) {
209
351
  const version = typeof personaMeta["version"] === "string" ? personaMeta["version"] : typeof sharedMeta["default_version"] === "string" ? sharedMeta["default_version"] : "0.0.0";
210
352
  const merged = {
211
353
  ...sharedMeta,
@@ -230,19 +372,42 @@ function buildContext(personaMeta, sharedMeta, agentMap = {}) {
230
372
  const ccFileName = merged["cc_file_name"];
231
373
  merged["cc_file_name_stem"] = ccFileName.replace(/\.md$/, "");
232
374
  }
375
+ if (!("da_file_name_stem" in merged) && typeof merged["da_file_name"] === "string") {
376
+ const daFileName = merged["da_file_name"];
377
+ merged["da_file_name_stem"] = daFileName.replace(/\.md$/, "");
378
+ }
379
+ if (typeof merged["da_file_name"] === "string") {
380
+ const daTools = Array.isArray(merged["da_tools"]) ? merged["da_tools"] : tools;
381
+ if (!("da_tools_list" in merged)) {
382
+ merged["da_tools_list"] = serializeToolsList(daTools);
383
+ }
384
+ if (!("da_tools_json" in merged)) {
385
+ merged["da_tools_json"] = serializeTools(daTools);
386
+ }
387
+ }
233
388
  for (const [key, value] of Object.entries(agentMap)) {
234
389
  if (!(key in merged)) {
235
390
  merged[key] = value;
236
391
  }
237
392
  }
393
+ if (target !== void 0) {
394
+ if (registry && registry.has(target)) {
395
+ const flags = registry.get(target).contextFlags ?? {};
396
+ for (const [key, value] of Object.entries(flags)) {
397
+ merged[key] = value;
398
+ }
399
+ } else {
400
+ merged[`target_${target.replace(/-/g, "_")}`] = true;
401
+ }
402
+ }
238
403
  return merged;
239
404
  }
240
- async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target, agentMap = {}) {
405
+ async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target, agentMap = {}, registry = defaultRegistry) {
241
406
  const personaMeta = await loadPersonaYaml(personaYamlPath);
242
- let context = buildContext(personaMeta, sharedMeta, agentMap);
407
+ let context = buildContext(personaMeta, sharedMeta, agentMap, target, registry);
243
408
  const personaMetaTyped = personaMeta;
244
- context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig);
245
- const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter);
409
+ context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig, target);
410
+ const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter, registry);
246
411
  const contentBasename = path2__default.default.basename(personaYamlPath, ".yaml") + ".md";
247
412
  const frontmatter = renderFrontmatter(fmTemplate, context, contentBasename);
248
413
  const contentSubdir = suiteConfig.contentSubdir ?? "content";
@@ -260,15 +425,10 @@ ${body}
260
425
  `);
261
426
  output = runPostRender(plugins, output, personaMetaTyped, target);
262
427
  const validationResults = runValidate(plugins, personaMetaTyped, suiteConfig, target);
263
- const outputDir = target === "vscode" ? suiteConfig.outVscode : suiteConfig.outClaudeCode;
264
- let outputBasename;
265
- if (target === "vscode" && typeof context["vs_file_name"] === "string") {
266
- outputBasename = context["vs_file_name"];
267
- } else if (target === "claude-code" && typeof context["cc_file_name"] === "string") {
268
- outputBasename = context["cc_file_name"];
269
- } else {
270
- outputBasename = contentBasename;
271
- }
428
+ const def = registry.has(target) ? registry.get(target) : void 0;
429
+ const outputDir = resolveOutputDir(target, suiteConfig, def);
430
+ const fnKey = def?.filenameContextKey;
431
+ const outputBasename = fnKey && typeof context[fnKey] === "string" ? context[fnKey] : contentBasename;
272
432
  const outputPath = path2__default.default.join(outputDir, outputBasename);
273
433
  const check = config.check ?? false;
274
434
  let written = false;
@@ -287,7 +447,7 @@ ${body}
287
447
  written
288
448
  };
289
449
  }
290
- async function buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap = {}) {
450
+ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap = {}, registry = defaultRegistry) {
291
451
  const metaSubdir = suiteConfig.metaSubdir ?? "meta";
292
452
  const sharedYamlPath = path2__default.default.join(suiteConfig.srcDir, metaSubdir, "_shared.yaml");
293
453
  const sharedMeta = await loadRawYaml(sharedYamlPath);
@@ -313,7 +473,8 @@ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agent
313
473
  config,
314
474
  plugins,
315
475
  target,
316
- agentMap
476
+ agentMap,
477
+ registry
317
478
  );
318
479
  results.push(result);
319
480
  }
@@ -321,12 +482,13 @@ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agent
321
482
  }
322
483
  async function build(config) {
323
484
  const plugins = config.plugins ?? [];
324
- const targets = config.targets ?? ["vscode", "claude-code"];
485
+ const registry = config.targetRegistry ?? defaultRegistry;
486
+ const targets = config.targets ?? registry.names().filter((n) => registry.get(n).defaultEnabled !== false);
325
487
  const allResults = [];
326
488
  const agentMap = await buildAgentNameMap(config);
327
489
  for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
328
490
  for (const target of targets) {
329
- const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap);
491
+ const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap, registry);
330
492
  allResults.push(...suiteResults);
331
493
  }
332
494
  }