@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/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # AI Persona Builder
2
2
 
3
- Build AI persona instruction files for **VS Code Chat** and **Claude Code** from YAML metadata and Markdown templates — with zero configuration friction.
3
+ Build AI persona instruction files for **VS Code Chat**, **Claude Code** and **LangGraph Deep Agents** from YAML metadata and Markdown templates — with zero configuration friction.
4
4
 
5
5
  Define your personas once as simple YAML + Markdown sources, and the library generates correctly formatted instruction files for both IDEs. A plugin system lets you inject custom frontmatter, run validators, or post-process output without touching the core engine.
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
@@ -24,6 +25,9 @@ Define your personas once as simple YAML + Markdown sources, and the library gen
24
25
  npm install @mistralys/persona-builder
25
26
  ```
26
27
 
28
+ - View on NPM: https://www.npmjs.com/package/@mistralys/persona-builder
29
+ - View on Github: https://github.com/Mistralys/ai-persona-builder
30
+
27
31
  ### Programmatic API
28
32
 
29
33
  ```ts
@@ -34,8 +38,10 @@ const summary = await build({
34
38
  suites: {
35
39
  'my-suite': {
36
40
  srcDir: path.resolve('./personas/my-suite'),
37
- outVscode: path.resolve('./dist/vscode'),
38
- outClaudeCode: path.resolve('./dist/claude-code'),
41
+ outputDirs: {
42
+ vscode: path.resolve('./dist/vscode'),
43
+ 'claude-code': path.resolve('./dist/claude-code'),
44
+ },
39
45
  },
40
46
  },
41
47
  sharedPartialsDir: path.resolve('./personas/shared/partials'),
@@ -75,6 +81,7 @@ See the [CLI docs](docs/cli.md) for config file format and all flags.
75
81
  | [Configuration Reference](docs/configuration.md) | `BuildConfig`, `SuiteConfig`, and `BuildSummary` fields |
76
82
  | [CLI Reference](docs/cli.md) | Command-line flags, config file format, and common patterns |
77
83
  | [Public API](docs/api.md) | All exported types and functions |
84
+ | [Project Manifest](docs/agents/project-manifest/README.md) | Canonical documentation for AI agent sessions |
78
85
 
79
86
  ## 🔌 Plugins
80
87
 
@@ -96,7 +103,7 @@ MIT
96
103
 
97
104
  ## Release Workflow
98
105
 
99
- 1. Add changelog entries
106
+ 1. Add changelog entries (do not change package.json version)
100
107
  2. `npm version 0.0.0` - Updates package and lock versions + commit
101
108
  3. `npm publish` - Publish version on NPM
102
109
  4. `git push origin 0.0.0` - Add the tag in GIT
package/dist/cli.cjs CHANGED
@@ -27,19 +27,48 @@ function resolvePartials(text, partialsMap, depth = 0) {
27
27
  }
28
28
 
29
29
  // src/engine/conditionals.ts
30
+ var NO_NESTED_IF = String.raw`(?:(?!\{\{#if\b)[\s\S])*?`;
31
+ var ELSE_IF_PATTERN = new RegExp(
32
+ String.raw`\{\{else if (\w+)\}\}(${NO_NESTED_IF})\{\{\/if\}\}`,
33
+ "g"
34
+ );
35
+ function resolveElseIf(text) {
36
+ if (!text.includes("{{else if ")) {
37
+ return text;
38
+ }
39
+ let result = text;
40
+ let prev;
41
+ do {
42
+ prev = result;
43
+ result = result.replace(
44
+ ELSE_IF_PATTERN,
45
+ (_match, flag, content) => `{{else}}{{#if ${flag}}}${content}{{/if}}{{/if}}`
46
+ );
47
+ } while (result !== prev);
48
+ return result;
49
+ }
30
50
  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
- }
51
+ const normalized = resolveElseIf(text);
52
+ const pattern = new RegExp(
53
+ String.raw`\n*\{\{#if (\w+)\}\}(${NO_NESTED_IF})` + String.raw`(?:\{\{else\}\}(${NO_NESTED_IF}))?\{\{\/if\}\}\n*`,
54
+ "g"
42
55
  );
56
+ const resolve = (_match, flag, inner, elseInner) => {
57
+ if (context[flag]) {
58
+ return "\n" + inner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
59
+ }
60
+ if (elseInner !== void 0) {
61
+ return "\n" + elseInner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
62
+ }
63
+ return "\n";
64
+ };
65
+ let result = normalized;
66
+ let prev;
67
+ do {
68
+ prev = result;
69
+ result = result.replace(pattern, resolve);
70
+ } while (result !== prev);
71
+ return result;
43
72
  }
44
73
 
45
74
  // src/engine/variables.ts
@@ -98,11 +127,11 @@ function runSuiteInit(plugins, suite, sharedMeta) {
98
127
  }
99
128
  }
100
129
  }
101
- function runBuildContext(plugins, ctx, persona, suite) {
130
+ function runBuildContext(plugins, ctx, persona, suite, target) {
102
131
  let accumulated = ctx;
103
132
  for (const plugin of plugins) {
104
133
  if (typeof plugin.onBuildContext === "function") {
105
- accumulated = plugin.onBuildContext(accumulated, persona, suite);
134
+ accumulated = plugin.onBuildContext(accumulated, persona, suite, target);
106
135
  }
107
136
  }
108
137
  return accumulated;
@@ -127,7 +156,10 @@ function runValidate(plugins, persona, suite, target) {
127
156
  return results;
128
157
  }
129
158
 
130
- // src/builders/frontmatter.ts
159
+ // src/targets/types.ts
160
+ var TARGET_VSCODE = "vscode";
161
+ var TARGET_CLAUDE_CODE = "claude-code";
162
+ var TARGET_DEEP_AGENTS = "deep-agents";
131
163
  var DEFAULT_FRONTMATTER_VSCODE = `---
132
164
  name: '{{name}} v{{version}}'
133
165
  description: '{{description}}'
@@ -140,7 +172,13 @@ model: {{cc_model}}
140
172
  memory: {{cc_memory}}
141
173
  allowedTools: [{{cc_tools_list}}]
142
174
  ---`;
143
- function resolveFrontmatterTemplate(target, plugins, configTemplates) {
175
+ var DEFAULT_FRONTMATTER_DEEP_AGENTS = `---
176
+ name: {{name}}
177
+ description: {{description}}
178
+ ---`;
179
+
180
+ // src/builders/frontmatter.ts
181
+ function resolveFrontmatterTemplate(target, plugins, configTemplates, registry) {
144
182
  for (const plugin of plugins) {
145
183
  if (plugin.frontmatterTemplates && target in plugin.frontmatterTemplates) {
146
184
  const tpl = plugin.frontmatterTemplates[target];
@@ -151,7 +189,10 @@ function resolveFrontmatterTemplate(target, plugins, configTemplates) {
151
189
  const tpl = configTemplates[target];
152
190
  if (tpl !== void 0) return tpl;
153
191
  }
154
- return target === "vscode" ? DEFAULT_FRONTMATTER_VSCODE : DEFAULT_FRONTMATTER_CLAUDE_CODE;
192
+ if (registry && registry.has(target)) {
193
+ return registry.get(target).defaultFrontmatter;
194
+ }
195
+ return DEFAULT_FRONTMATTER_VSCODE;
155
196
  }
156
197
  function renderFrontmatter(template, context, filename) {
157
198
  let rendered = resolveConditionals(template, context);
@@ -159,6 +200,112 @@ function renderFrontmatter(template, context, filename) {
159
200
  return rendered;
160
201
  }
161
202
 
203
+ // src/targets/registry.ts
204
+ var TargetRegistry = class _TargetRegistry {
205
+ // Map preserves insertion order — names() and allDefinitions() are
206
+ // therefore deterministic and match registration sequence. This is
207
+ // intentional: the built-in registry guarantees ['vscode', 'claude-code']
208
+ // ordering for the default targets (AC-2).
209
+ _definitions = /* @__PURE__ */ new Map();
210
+ /**
211
+ * Register a new target definition.
212
+ *
213
+ * @param definition The target descriptor to register.
214
+ * @throws {Error} If a target with the same `name` is already registered.
215
+ */
216
+ register(definition) {
217
+ if (this._definitions.has(definition.name)) {
218
+ throw new Error(
219
+ `TargetRegistry: target "${definition.name}" is already registered. Use a unique name or remove the existing registration first.`
220
+ );
221
+ }
222
+ this._definitions.set(definition.name, definition);
223
+ }
224
+ /**
225
+ * Retrieve a registered target definition by name.
226
+ *
227
+ * Returns a shallow copy — mutating the returned object does not affect
228
+ * the registry's internal state.
229
+ *
230
+ * @param name The target name to look up.
231
+ * @returns A shallow copy of the matching TargetDefinition.
232
+ * @throws {Error} If no target with the given name is registered.
233
+ */
234
+ get(name) {
235
+ const def = this._definitions.get(name);
236
+ if (!def) {
237
+ const known = this.names().join(", ") || "(none)";
238
+ throw new Error(
239
+ `TargetRegistry: target "${name}" is not registered. Registered targets: ${known}.`
240
+ );
241
+ }
242
+ return { ...def };
243
+ }
244
+ /**
245
+ * Returns `true` if a target with the given name is registered.
246
+ *
247
+ * @param name The target name to check.
248
+ */
249
+ has(name) {
250
+ return this._definitions.has(name);
251
+ }
252
+ /**
253
+ * Returns the names of all registered targets, in registration order.
254
+ */
255
+ names() {
256
+ return Array.from(this._definitions.keys());
257
+ }
258
+ /**
259
+ * Returns all registered TargetDefinition objects, in registration order.
260
+ *
261
+ * Returns shallow copies — mutating a returned definition does not affect
262
+ * the registry's internal state.
263
+ */
264
+ allDefinitions() {
265
+ return Array.from(this._definitions.values()).map((def) => ({ ...def }));
266
+ }
267
+ /**
268
+ * Returns a new TargetRegistry pre-populated with the same definitions.
269
+ *
270
+ * Useful for test isolation: clone the `defaultRegistry` to get an
271
+ * independent copy that can be mutated without affecting the singleton.
272
+ */
273
+ clone() {
274
+ const copy = new _TargetRegistry();
275
+ for (const def of this._definitions.values()) {
276
+ copy.register({ ...def });
277
+ }
278
+ return copy;
279
+ }
280
+ };
281
+
282
+ // src/targets/built-in.ts
283
+ var defaultRegistry = new TargetRegistry();
284
+ defaultRegistry.register({
285
+ name: TARGET_VSCODE,
286
+ outputDirKey: "vscode",
287
+ filenameContextKey: "vs_file_name",
288
+ defaultFrontmatter: DEFAULT_FRONTMATTER_VSCODE,
289
+ contextFlags: { target_vscode: true },
290
+ defaultEnabled: true
291
+ });
292
+ defaultRegistry.register({
293
+ name: TARGET_CLAUDE_CODE,
294
+ outputDirKey: "claude-code",
295
+ filenameContextKey: "cc_file_name",
296
+ defaultFrontmatter: DEFAULT_FRONTMATTER_CLAUDE_CODE,
297
+ contextFlags: { target_claude_code: true },
298
+ defaultEnabled: true
299
+ });
300
+ defaultRegistry.register({
301
+ name: TARGET_DEEP_AGENTS,
302
+ outputDirKey: "deep-agents",
303
+ filenameContextKey: "da_file_name",
304
+ defaultFrontmatter: DEFAULT_FRONTMATTER_DEEP_AGENTS,
305
+ contextFlags: { target_deep_agents: true },
306
+ defaultEnabled: false
307
+ });
308
+
162
309
  // src/builders/persona-builder.ts
163
310
  async function discoverSuitePersonaYamls(suiteConfig) {
164
311
  const metaSubdir = suiteConfig.metaSubdir ?? "meta";
@@ -186,6 +333,18 @@ async function loadPersonaYaml(yamlPath) {
186
333
  }
187
334
  return record;
188
335
  }
336
+ function resolveOutputDir(target, suiteConfig, definition) {
337
+ const merged = {};
338
+ if (suiteConfig.outVscode) merged["vscode"] = suiteConfig.outVscode;
339
+ if (suiteConfig.outClaudeCode) merged["claude-code"] = suiteConfig.outClaudeCode;
340
+ if (suiteConfig.outputDirs) Object.assign(merged, suiteConfig.outputDirs);
341
+ const lookupKey = definition?.outputDirKey ?? target;
342
+ const dir = merged[lookupKey];
343
+ if (dir) return dir;
344
+ throw new Error(
345
+ `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.`
346
+ );
347
+ }
189
348
  async function buildAgentNameMap(config) {
190
349
  const agentMap = {};
191
350
  for (const [, suiteConfig] of Object.entries(config.suites)) {
@@ -199,13 +358,16 @@ async function buildAgentNameMap(config) {
199
358
  const slug = typeof persona["slug"] === "string" ? persona["slug"] : path2__default.default.basename(yamlPath, ".yaml");
200
359
  const name = typeof persona["name"] === "string" ? persona["name"] : slug;
201
360
  const version = typeof persona["version"] === "string" ? persona["version"] : defaultVersion;
202
- const key = `agent_${slug.replace(/-/g, "_")}`;
361
+ const underscoredSlug = slug.replace(/-/g, "_");
362
+ const key = `agent_${underscoredSlug}`;
203
363
  agentMap[key] = `${name} v${version}`;
364
+ const slugKey = `agent_slug_${underscoredSlug}`;
365
+ agentMap[slugKey] = slug;
204
366
  }
205
367
  }
206
368
  return agentMap;
207
369
  }
208
- function buildContext(personaMeta, sharedMeta, agentMap = {}) {
370
+ function buildContext(personaMeta, sharedMeta, agentMap = {}, target, registry) {
209
371
  const version = typeof personaMeta["version"] === "string" ? personaMeta["version"] : typeof sharedMeta["default_version"] === "string" ? sharedMeta["default_version"] : "0.0.0";
210
372
  const merged = {
211
373
  ...sharedMeta,
@@ -230,19 +392,42 @@ function buildContext(personaMeta, sharedMeta, agentMap = {}) {
230
392
  const ccFileName = merged["cc_file_name"];
231
393
  merged["cc_file_name_stem"] = ccFileName.replace(/\.md$/, "");
232
394
  }
395
+ if (!("da_file_name_stem" in merged) && typeof merged["da_file_name"] === "string") {
396
+ const daFileName = merged["da_file_name"];
397
+ merged["da_file_name_stem"] = daFileName.replace(/\.md$/, "");
398
+ }
399
+ if (typeof merged["da_file_name"] === "string") {
400
+ const daTools = Array.isArray(merged["da_tools"]) ? merged["da_tools"] : tools;
401
+ if (!("da_tools_list" in merged)) {
402
+ merged["da_tools_list"] = serializeToolsList(daTools);
403
+ }
404
+ if (!("da_tools_json" in merged)) {
405
+ merged["da_tools_json"] = serializeTools(daTools);
406
+ }
407
+ }
233
408
  for (const [key, value] of Object.entries(agentMap)) {
234
409
  if (!(key in merged)) {
235
410
  merged[key] = value;
236
411
  }
237
412
  }
413
+ if (target !== void 0) {
414
+ if (registry && registry.has(target)) {
415
+ const flags = registry.get(target).contextFlags ?? {};
416
+ for (const [key, value] of Object.entries(flags)) {
417
+ merged[key] = value;
418
+ }
419
+ } else {
420
+ merged[`target_${target.replace(/-/g, "_")}`] = true;
421
+ }
422
+ }
238
423
  return merged;
239
424
  }
240
- async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target, agentMap = {}) {
425
+ async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target, agentMap = {}, registry = defaultRegistry) {
241
426
  const personaMeta = await loadPersonaYaml(personaYamlPath);
242
- let context = buildContext(personaMeta, sharedMeta, agentMap);
427
+ let context = buildContext(personaMeta, sharedMeta, agentMap, target, registry);
243
428
  const personaMetaTyped = personaMeta;
244
- context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig);
245
- const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter);
429
+ context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig, target);
430
+ const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter, registry);
246
431
  const contentBasename = path2__default.default.basename(personaYamlPath, ".yaml") + ".md";
247
432
  const frontmatter = renderFrontmatter(fmTemplate, context, contentBasename);
248
433
  const contentSubdir = suiteConfig.contentSubdir ?? "content";
@@ -260,15 +445,10 @@ ${body}
260
445
  `);
261
446
  output = runPostRender(plugins, output, personaMetaTyped, target);
262
447
  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
- }
448
+ const def = registry.has(target) ? registry.get(target) : void 0;
449
+ const outputDir = resolveOutputDir(target, suiteConfig, def);
450
+ const fnKey = def?.filenameContextKey;
451
+ const outputBasename = fnKey && typeof context[fnKey] === "string" ? context[fnKey] : contentBasename;
272
452
  const outputPath = path2__default.default.join(outputDir, outputBasename);
273
453
  const check = config.check ?? false;
274
454
  let written = false;
@@ -287,7 +467,7 @@ ${body}
287
467
  written
288
468
  };
289
469
  }
290
- async function buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap = {}) {
470
+ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap = {}, registry = defaultRegistry) {
291
471
  const metaSubdir = suiteConfig.metaSubdir ?? "meta";
292
472
  const sharedYamlPath = path2__default.default.join(suiteConfig.srcDir, metaSubdir, "_shared.yaml");
293
473
  const sharedMeta = await loadRawYaml(sharedYamlPath);
@@ -313,7 +493,8 @@ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agent
313
493
  config,
314
494
  plugins,
315
495
  target,
316
- agentMap
496
+ agentMap,
497
+ registry
317
498
  );
318
499
  results.push(result);
319
500
  }
@@ -321,12 +502,13 @@ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agent
321
502
  }
322
503
  async function build(config) {
323
504
  const plugins = config.plugins ?? [];
324
- const targets = config.targets ?? ["vscode", "claude-code"];
505
+ const registry = config.targetRegistry ?? defaultRegistry;
506
+ const targets = config.targets ?? registry.names().filter((n) => registry.get(n).defaultEnabled !== false);
325
507
  const allResults = [];
326
508
  const agentMap = await buildAgentNameMap(config);
327
509
  for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
328
510
  for (const target of targets) {
329
- const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap);
511
+ const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target, agentMap, registry);
330
512
  allResults.push(...suiteResults);
331
513
  }
332
514
  }