@mistralys/persona-builder 2.1.3 → 2.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.
package/dist/cli.js CHANGED
@@ -19,19 +19,48 @@ function resolvePartials(text, partialsMap, depth = 0) {
19
19
  }
20
20
 
21
21
  // src/engine/conditionals.ts
22
+ var NO_NESTED_IF = String.raw`(?:(?!\{\{#if\b)[\s\S])*?`;
23
+ var ELSE_IF_PATTERN = new RegExp(
24
+ String.raw`\{\{else if (\w+)\}\}(${NO_NESTED_IF})\{\{\/if\}\}`,
25
+ "g"
26
+ );
27
+ function resolveElseIf(text) {
28
+ if (!text.includes("{{else if ")) {
29
+ return text;
30
+ }
31
+ let result = text;
32
+ let prev;
33
+ do {
34
+ prev = result;
35
+ result = result.replace(
36
+ ELSE_IF_PATTERN,
37
+ (_match, flag, content) => `{{else}}{{#if ${flag}}}${content}{{/if}}{{/if}}`
38
+ );
39
+ } while (result !== prev);
40
+ return result;
41
+ }
22
42
  function resolveConditionals(text, context) {
23
- return text.replace(
24
- /\n*\{\{#if (\w+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}\n*/g,
25
- (_match, flag, inner, elseInner) => {
26
- if (context[flag]) {
27
- return "\n" + inner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
28
- }
29
- if (elseInner !== void 0) {
30
- return "\n" + elseInner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
31
- }
32
- return "\n";
33
- }
43
+ const normalized = resolveElseIf(text);
44
+ const pattern = new RegExp(
45
+ String.raw`\n*\{\{#if (\w+)\}\}(${NO_NESTED_IF})` + String.raw`(?:\{\{else\}\}(${NO_NESTED_IF}))?\{\{\/if\}\}\n*`,
46
+ "g"
34
47
  );
48
+ const resolve = (_match, flag, inner, elseInner) => {
49
+ if (context[flag]) {
50
+ return "\n" + inner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
51
+ }
52
+ if (elseInner !== void 0) {
53
+ return "\n" + elseInner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
54
+ }
55
+ return "\n";
56
+ };
57
+ let result = normalized;
58
+ let prev;
59
+ do {
60
+ prev = result;
61
+ result = result.replace(pattern, resolve);
62
+ } while (result !== prev);
63
+ return result;
35
64
  }
36
65
 
37
66
  // src/engine/variables.ts
@@ -90,11 +119,11 @@ function runSuiteInit(plugins, suite, sharedMeta) {
90
119
  }
91
120
  }
92
121
  }
93
- function runBuildContext(plugins, ctx, persona, suite) {
122
+ function runBuildContext(plugins, ctx, persona, suite, target) {
94
123
  let accumulated = ctx;
95
124
  for (const plugin of plugins) {
96
125
  if (typeof plugin.onBuildContext === "function") {
97
- accumulated = plugin.onBuildContext(accumulated, persona, suite);
126
+ accumulated = plugin.onBuildContext(accumulated, persona, suite, target);
98
127
  }
99
128
  }
100
129
  return accumulated;
@@ -119,7 +148,10 @@ function runValidate(plugins, persona, suite, target) {
119
148
  return results;
120
149
  }
121
150
 
122
- // src/builders/frontmatter.ts
151
+ // src/targets/types.ts
152
+ var TARGET_VSCODE = "vscode";
153
+ var TARGET_CLAUDE_CODE = "claude-code";
154
+ var TARGET_DEEP_AGENTS = "deep-agents";
123
155
  var DEFAULT_FRONTMATTER_VSCODE = `---
124
156
  name: '{{name}} v{{version}}'
125
157
  description: '{{description}}'
@@ -132,7 +164,13 @@ model: {{cc_model}}
132
164
  memory: {{cc_memory}}
133
165
  allowedTools: [{{cc_tools_list}}]
134
166
  ---`;
135
- function resolveFrontmatterTemplate(target, plugins, configTemplates) {
167
+ var DEFAULT_FRONTMATTER_DEEP_AGENTS = `---
168
+ name: {{name}}
169
+ description: {{description}}
170
+ ---`;
171
+
172
+ // src/builders/frontmatter.ts
173
+ function resolveFrontmatterTemplate(target, plugins, configTemplates, registry) {
136
174
  for (const plugin of plugins) {
137
175
  if (plugin.frontmatterTemplates && target in plugin.frontmatterTemplates) {
138
176
  const tpl = plugin.frontmatterTemplates[target];
@@ -143,7 +181,10 @@ function resolveFrontmatterTemplate(target, plugins, configTemplates) {
143
181
  const tpl = configTemplates[target];
144
182
  if (tpl !== void 0) return tpl;
145
183
  }
146
- return target === "vscode" ? DEFAULT_FRONTMATTER_VSCODE : DEFAULT_FRONTMATTER_CLAUDE_CODE;
184
+ if (registry && registry.has(target)) {
185
+ return registry.get(target).defaultFrontmatter;
186
+ }
187
+ return DEFAULT_FRONTMATTER_VSCODE;
147
188
  }
148
189
  function renderFrontmatter(template, context, filename) {
149
190
  let rendered = resolveConditionals(template, context);
@@ -151,6 +192,112 @@ function renderFrontmatter(template, context, filename) {
151
192
  return rendered;
152
193
  }
153
194
 
195
+ // src/targets/registry.ts
196
+ var TargetRegistry = class _TargetRegistry {
197
+ // Map preserves insertion order — names() and allDefinitions() are
198
+ // therefore deterministic and match registration sequence. This is
199
+ // intentional: the built-in registry guarantees ['vscode', 'claude-code']
200
+ // ordering for the default targets (AC-2).
201
+ _definitions = /* @__PURE__ */ new Map();
202
+ /**
203
+ * Register a new target definition.
204
+ *
205
+ * @param definition The target descriptor to register.
206
+ * @throws {Error} If a target with the same `name` is already registered.
207
+ */
208
+ register(definition) {
209
+ if (this._definitions.has(definition.name)) {
210
+ throw new Error(
211
+ `TargetRegistry: target "${definition.name}" is already registered. Use a unique name or remove the existing registration first.`
212
+ );
213
+ }
214
+ this._definitions.set(definition.name, definition);
215
+ }
216
+ /**
217
+ * Retrieve a registered target definition by name.
218
+ *
219
+ * Returns a shallow copy — mutating the returned object does not affect
220
+ * the registry's internal state.
221
+ *
222
+ * @param name The target name to look up.
223
+ * @returns A shallow copy of the matching TargetDefinition.
224
+ * @throws {Error} If no target with the given name is registered.
225
+ */
226
+ get(name) {
227
+ const def = this._definitions.get(name);
228
+ if (!def) {
229
+ const known = this.names().join(", ") || "(none)";
230
+ throw new Error(
231
+ `TargetRegistry: target "${name}" is not registered. Registered targets: ${known}.`
232
+ );
233
+ }
234
+ return { ...def };
235
+ }
236
+ /**
237
+ * Returns `true` if a target with the given name is registered.
238
+ *
239
+ * @param name The target name to check.
240
+ */
241
+ has(name) {
242
+ return this._definitions.has(name);
243
+ }
244
+ /**
245
+ * Returns the names of all registered targets, in registration order.
246
+ */
247
+ names() {
248
+ return Array.from(this._definitions.keys());
249
+ }
250
+ /**
251
+ * Returns all registered TargetDefinition objects, in registration order.
252
+ *
253
+ * Returns shallow copies — mutating a returned definition does not affect
254
+ * the registry's internal state.
255
+ */
256
+ allDefinitions() {
257
+ return Array.from(this._definitions.values()).map((def) => ({ ...def }));
258
+ }
259
+ /**
260
+ * Returns a new TargetRegistry pre-populated with the same definitions.
261
+ *
262
+ * Useful for test isolation: clone the `defaultRegistry` to get an
263
+ * independent copy that can be mutated without affecting the singleton.
264
+ */
265
+ clone() {
266
+ const copy = new _TargetRegistry();
267
+ for (const def of this._definitions.values()) {
268
+ copy.register({ ...def });
269
+ }
270
+ return copy;
271
+ }
272
+ };
273
+
274
+ // src/targets/built-in.ts
275
+ var defaultRegistry = new TargetRegistry();
276
+ defaultRegistry.register({
277
+ name: TARGET_VSCODE,
278
+ outputDirKey: "vscode",
279
+ filenameContextKey: "vs_file_name",
280
+ defaultFrontmatter: DEFAULT_FRONTMATTER_VSCODE,
281
+ contextFlags: { target_vscode: true },
282
+ defaultEnabled: true
283
+ });
284
+ defaultRegistry.register({
285
+ name: TARGET_CLAUDE_CODE,
286
+ outputDirKey: "claude-code",
287
+ filenameContextKey: "cc_file_name",
288
+ defaultFrontmatter: DEFAULT_FRONTMATTER_CLAUDE_CODE,
289
+ contextFlags: { target_claude_code: true },
290
+ defaultEnabled: true
291
+ });
292
+ defaultRegistry.register({
293
+ name: TARGET_DEEP_AGENTS,
294
+ outputDirKey: "deep-agents",
295
+ filenameContextKey: "da_file_name",
296
+ defaultFrontmatter: DEFAULT_FRONTMATTER_DEEP_AGENTS,
297
+ contextFlags: { target_deep_agents: true },
298
+ defaultEnabled: false
299
+ });
300
+
154
301
  // src/builders/persona-builder.ts
155
302
  async function discoverSuitePersonaYamls(suiteConfig) {
156
303
  const metaSubdir = suiteConfig.metaSubdir ?? "meta";
@@ -178,6 +325,18 @@ async function loadPersonaYaml(yamlPath) {
178
325
  }
179
326
  return record;
180
327
  }
328
+ function resolveOutputDir(target, suiteConfig, definition) {
329
+ const merged = {};
330
+ if (suiteConfig.outVscode) merged["vscode"] = suiteConfig.outVscode;
331
+ if (suiteConfig.outClaudeCode) merged["claude-code"] = suiteConfig.outClaudeCode;
332
+ if (suiteConfig.outputDirs) Object.assign(merged, suiteConfig.outputDirs);
333
+ const lookupKey = definition?.outputDirKey ?? target;
334
+ const dir = merged[lookupKey];
335
+ if (dir) return dir;
336
+ throw new Error(
337
+ `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.`
338
+ );
339
+ }
181
340
  async function buildAgentNameMap(config) {
182
341
  const agentMap = {};
183
342
  for (const [, suiteConfig] of Object.entries(config.suites)) {
@@ -191,13 +350,16 @@ async function buildAgentNameMap(config) {
191
350
  const slug = typeof persona["slug"] === "string" ? persona["slug"] : path2.basename(yamlPath, ".yaml");
192
351
  const name = typeof persona["name"] === "string" ? persona["name"] : slug;
193
352
  const version = typeof persona["version"] === "string" ? persona["version"] : defaultVersion;
194
- const key = `agent_${slug.replace(/-/g, "_")}`;
353
+ const underscoredSlug = slug.replace(/-/g, "_");
354
+ const key = `agent_${underscoredSlug}`;
195
355
  agentMap[key] = `${name} v${version}`;
356
+ const slugKey = `agent_slug_${underscoredSlug}`;
357
+ agentMap[slugKey] = slug;
196
358
  }
197
359
  }
198
360
  return agentMap;
199
361
  }
200
- function buildContext(personaMeta, sharedMeta, agentMap = {}) {
362
+ function buildContext(personaMeta, sharedMeta, agentMap = {}, target, registry) {
201
363
  const version = typeof personaMeta["version"] === "string" ? personaMeta["version"] : typeof sharedMeta["default_version"] === "string" ? sharedMeta["default_version"] : "0.0.0";
202
364
  const merged = {
203
365
  ...sharedMeta,
@@ -222,19 +384,42 @@ function buildContext(personaMeta, sharedMeta, agentMap = {}) {
222
384
  const ccFileName = merged["cc_file_name"];
223
385
  merged["cc_file_name_stem"] = ccFileName.replace(/\.md$/, "");
224
386
  }
387
+ if (!("da_file_name_stem" in merged) && typeof merged["da_file_name"] === "string") {
388
+ const daFileName = merged["da_file_name"];
389
+ merged["da_file_name_stem"] = daFileName.replace(/\.md$/, "");
390
+ }
391
+ if (typeof merged["da_file_name"] === "string") {
392
+ const daTools = Array.isArray(merged["da_tools"]) ? merged["da_tools"] : tools;
393
+ if (!("da_tools_list" in merged)) {
394
+ merged["da_tools_list"] = serializeToolsList(daTools);
395
+ }
396
+ if (!("da_tools_json" in merged)) {
397
+ merged["da_tools_json"] = serializeTools(daTools);
398
+ }
399
+ }
225
400
  for (const [key, value] of Object.entries(agentMap)) {
226
401
  if (!(key in merged)) {
227
402
  merged[key] = value;
228
403
  }
229
404
  }
405
+ if (target !== void 0) {
406
+ if (registry && registry.has(target)) {
407
+ const flags = registry.get(target).contextFlags ?? {};
408
+ for (const [key, value] of Object.entries(flags)) {
409
+ merged[key] = value;
410
+ }
411
+ } else {
412
+ merged[`target_${target.replace(/-/g, "_")}`] = true;
413
+ }
414
+ }
230
415
  return merged;
231
416
  }
232
- async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target, agentMap = {}) {
417
+ async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target, agentMap = {}, registry = defaultRegistry) {
233
418
  const personaMeta = await loadPersonaYaml(personaYamlPath);
234
- let context = buildContext(personaMeta, sharedMeta, agentMap);
419
+ let context = buildContext(personaMeta, sharedMeta, agentMap, target, registry);
235
420
  const personaMetaTyped = personaMeta;
236
- context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig);
237
- const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter);
421
+ context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig, target);
422
+ const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter, registry);
238
423
  const contentBasename = path2.basename(personaYamlPath, ".yaml") + ".md";
239
424
  const frontmatter = renderFrontmatter(fmTemplate, context, contentBasename);
240
425
  const contentSubdir = suiteConfig.contentSubdir ?? "content";
@@ -252,15 +437,10 @@ ${body}
252
437
  `);
253
438
  output = runPostRender(plugins, output, personaMetaTyped, target);
254
439
  const validationResults = runValidate(plugins, personaMetaTyped, suiteConfig, target);
255
- const outputDir = target === "vscode" ? suiteConfig.outVscode : suiteConfig.outClaudeCode;
256
- let outputBasename;
257
- if (target === "vscode" && typeof context["vs_file_name"] === "string") {
258
- outputBasename = context["vs_file_name"];
259
- } else if (target === "claude-code" && typeof context["cc_file_name"] === "string") {
260
- outputBasename = context["cc_file_name"];
261
- } else {
262
- outputBasename = contentBasename;
263
- }
440
+ const def = registry.has(target) ? registry.get(target) : void 0;
441
+ const outputDir = resolveOutputDir(target, suiteConfig, def);
442
+ const fnKey = def?.filenameContextKey;
443
+ const outputBasename = fnKey && typeof context[fnKey] === "string" ? context[fnKey] : contentBasename;
264
444
  const outputPath = path2.join(outputDir, outputBasename);
265
445
  const check = config.check ?? false;
266
446
  let written = false;
@@ -279,7 +459,7 @@ ${body}
279
459
  written
280
460
  };
281
461
  }
282
- async function buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap = {}) {
462
+ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap = {}, registry = defaultRegistry) {
283
463
  const metaSubdir = suiteConfig.metaSubdir ?? "meta";
284
464
  const sharedYamlPath = path2.join(suiteConfig.srcDir, metaSubdir, "_shared.yaml");
285
465
  const sharedMeta = await loadRawYaml(sharedYamlPath);
@@ -305,7 +485,8 @@ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agent
305
485
  config,
306
486
  plugins,
307
487
  target,
308
- agentMap
488
+ agentMap,
489
+ registry
309
490
  );
310
491
  results.push(result);
311
492
  }
@@ -313,12 +494,13 @@ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agent
313
494
  }
314
495
  async function build(config) {
315
496
  const plugins = config.plugins ?? [];
316
- const targets = config.targets ?? ["vscode", "claude-code"];
497
+ const registry = config.targetRegistry ?? defaultRegistry;
498
+ const targets = config.targets ?? registry.names().filter((n) => registry.get(n).defaultEnabled !== false);
317
499
  const allResults = [];
318
500
  const agentMap = await buildAgentNameMap(config);
319
501
  for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
320
502
  for (const target of targets) {
321
- const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap);
503
+ const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap, registry);
322
504
  allResults.push(...suiteResults);
323
505
  }
324
506
  }