@metrio-ai/cli 1.3.1 → 1.4.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
@@ -31,6 +31,8 @@ metrio prompt list
31
31
  metrio version get 34 -o system.md # the system prompt as a file
32
32
  metrio version create 12 -f system.md --model-provider openai --model-name gpt-5
33
33
  metrio version get 34 -o v.yaml && metrio version update 34 -f v.yaml # round-trip
34
+ metrio version get 34 -o greeter.md --frontmatter # settings as YAML frontmatter + the prompt verbatim
35
+ metrio version create 12 -f greeter.md --frontmatter # one file carries the whole version
34
36
  metrio skill get 5 -o skill.md # frontmatter (name, description, references) + body
35
37
  metrio skill update 5 -f skill.md
36
38
  metrio workflow version create intake -f definition.json --activate
@@ -87,7 +87,9 @@ async function writeFile(data, opts, file) {
87
87
  const text = opts.textField ? record[opts.textField] : undefined;
88
88
  if (typeof text !== 'string')
89
89
  throw new errors_1.CliError(`-o ${file}: this result has no text body; use .json or .yaml`);
90
- const front = Object.fromEntries((opts.frontmatterKeys ?? []).filter((k) => record[k] !== undefined && record[k] !== null).map((k) => [k, record[k]]));
90
+ const front = Object.fromEntries((opts.frontmatterKeys ?? [])
91
+ .filter((k) => record[k] !== undefined && (record[k] !== null || (opts.frontmatterKeepNull ?? []).includes(k)))
92
+ .map((k) => [k, record[k]]));
91
93
  const head = Object.keys(front).length ? `---\n${(0, yaml_1.stringify)(front)}---\n\n` : '';
92
94
  body = `${head}${text}`;
93
95
  }
@@ -1,10 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MANAGE_GROUPS = exports.COMMANDS = void 0;
3
+ exports.MANAGE_GROUPS = exports.COMMANDS = exports.READ_ONLY_FRONTMATTER = void 0;
4
4
  exports.findCommand = findCommand;
5
5
  exports.groupHelp = groupHelp;
6
6
  exports.commandHelp = commandHelp;
7
7
  const P = (prop, label) => ({ prop, label });
8
+ /** Identity of the version a file came from: written for reference, never sent back. */
9
+ exports.READ_ONLY_FRONTMATTER = ['version_id', 'serial'];
10
+ const VERSION_FRONTMATTER = [
11
+ ...exports.READ_ONLY_FRONTMATTER, 'description', 'tier_key', 'model_provider', 'model_name',
12
+ 'output_format', 'model_settings', 'model_fallbacks', 'output_schema',
13
+ ];
14
+ /** get_version → the create_version/update_version field names the frontmatter uses. */
15
+ function versionFrontmatterView(r) {
16
+ const tier = r.model_tier?.tier_key;
17
+ // A file describes the whole version: "no schema" / "no fallbacks" are written explicitly, so
18
+ // update -f clears them instead of keeping what the target version had (omission keeps).
19
+ const view = {
20
+ ...r,
21
+ version_id: r.id,
22
+ output_schema: r.output_schema ?? null,
23
+ model_fallbacks: r.model_fallbacks ?? [],
24
+ };
25
+ if (typeof tier === 'string' && tier) {
26
+ view.tier_key = tier;
27
+ delete view.model_provider;
28
+ delete view.model_name;
29
+ }
30
+ return view;
31
+ }
32
+ const VERSION_FILE = {
33
+ optionalFrontmatterKeys: VERSION_FRONTMATTER,
34
+ frontmatterView: versionFrontmatterView,
35
+ frontmatterKeepNull: ['output_schema'],
36
+ };
8
37
  const PROMPT = P('prompt_external_id', '<prompt-id>');
9
38
  const VERSION_ID = P('version_id', '<version-id>');
10
39
  const SKILL = P('skill_id', '<skill-id>');
@@ -28,9 +57,9 @@ exports.COMMANDS = [
28
57
  { path: ['prompt', 'set-mcp-tools'], tool: 'set_prompt_mcp_tools', summary: 'Set the MCP tool allowlist (-f tools.yaml)', args: [PROMPT] },
29
58
  { path: ['prompt', 'diff'], tool: 'diff_prompt', summary: 'Diff two versions', args: [PROMPT, P('from_serial', '<from>'), P('to_serial', '<to>')] },
30
59
  { path: ['version', 'list'], tool: 'list_versions', summary: 'List versions of a prompt', args: [PROMPT] },
31
- { path: ['version', 'get'], tool: 'get_version', summary: 'Get a version (-o v.yaml | system.md)', args: [VERSION_ID], textField: 'system_prompt' },
32
- { path: ['version', 'create'], tool: 'create_version', summary: 'Create a version (-f system.md | v.yaml)', args: [PROMPT], textField: 'system_prompt' },
33
- { path: ['version', 'update'], tool: 'update_version', summary: 'Update a version (-f system.md | v.yaml)', args: [VERSION_ID], textField: 'system_prompt' },
60
+ { path: ['version', 'get'], tool: 'get_version', summary: 'Get a version (-o v.yaml | system.md [--frontmatter])', args: [VERSION_ID], textField: 'system_prompt', ...VERSION_FILE },
61
+ { path: ['version', 'create'], tool: 'create_version', summary: 'Create a version (-f system.md [--frontmatter] | v.yaml)', args: [PROMPT], textField: 'system_prompt', ...VERSION_FILE },
62
+ { path: ['version', 'update'], tool: 'update_version', summary: 'Update a version (-f system.md [--frontmatter] | v.yaml)', args: [VERSION_ID], textField: 'system_prompt', ...VERSION_FILE },
34
63
  { path: ['version', 'create-batch'], tool: 'create_versions', summary: 'Create versions for several prompts (-f versions.jsonl)', wrap: 'versions' },
35
64
  { path: ['skill', 'list'], tool: 'list_skills', summary: 'List skills' },
36
65
  { path: ['skill', 'get'], tool: 'get_skill', summary: 'Get a skill (-o skill.md)', args: [SKILL], textField: 'content', frontmatterKeys: ['name', 'description', 'references'] },
@@ -107,10 +136,16 @@ function commandHelp(spec) {
107
136
  ];
108
137
  if (spec.destructive)
109
138
  lines.push(' --yes Confirm the delete');
139
+ if (spec.optionalFrontmatterKeys) {
140
+ lines.push(' --frontmatter .md files carry the version settings as YAML frontmatter (see below)');
141
+ }
110
142
  if (spec.textField)
111
143
  lines.push('', `A .md/.txt file fills ${spec.textField}.`);
112
144
  if (spec.frontmatterKeys)
113
145
  lines.push(`Its YAML frontmatter may set: ${spec.frontmatterKeys.join(', ')}.`);
146
+ if (spec.optionalFrontmatterKeys) {
147
+ lines.push(`With --frontmatter, a .md file starts with YAML frontmatter (${spec.optionalFrontmatterKeys.join(', ')})`, `and the rest is ${spec.textField} verbatim. ${exports.READ_ONLY_FRONTMATTER.join(' and ')} are written by -o and ignored by -f.`, 'Without --frontmatter the whole file is the text, so a prompt that starts with --- stays intact.');
148
+ }
114
149
  if (spec.wrap)
115
150
  lines.push('', `A file without a "${spec.wrap}" key is sent as ${spec.wrap}.`);
116
151
  if (spec.downloadField)
@@ -10,7 +10,7 @@ const registry_1 = require("./registry");
10
10
  const workspace_1 = require("../sync/workspace");
11
11
  const schema_1 = require("./schema");
12
12
  function parse(rest) {
13
- const parsed = { tokens: [], json: false, yes: false };
13
+ const parsed = { tokens: [], json: false, yes: false, frontmatter: false };
14
14
  const take = (i, name) => {
15
15
  const v = rest[i + 1];
16
16
  if (v === undefined)
@@ -38,6 +38,8 @@ function parse(rest) {
38
38
  parsed.json = true;
39
39
  else if (arg === '--yes')
40
40
  parsed.yes = true;
41
+ else if (arg === '--frontmatter')
42
+ parsed.frontmatter = true;
41
43
  else if (arg.startsWith('--')) {
42
44
  const eq = arg.indexOf('=');
43
45
  parsed.tokens.push(eq === -1 ? { flag: arg.slice(2) } : { flag: arg.slice(2, eq), value: arg.slice(eq + 1) });
@@ -47,13 +49,17 @@ function parse(rest) {
47
49
  }
48
50
  return parsed;
49
51
  }
50
- function fromFile(spec, file, readStdin) {
51
- const loaded = (0, input_1.loadInputFile)(file, readStdin, { frontmatter: !!spec.frontmatterKeys, stdinText: !!spec.textField });
52
+ function fromFile(spec, file, frontmatter, readStdin) {
53
+ const loaded = (0, input_1.loadInputFile)(file, readStdin, { frontmatter: !!spec.frontmatterKeys || frontmatter, stdinText: !!spec.textField });
52
54
  const cmd = `metrio ${spec.path.join(' ')}`;
53
55
  if (loaded.kind === 'text') {
54
56
  if (!spec.textField)
55
57
  throw new errors_1.CliError(`${cmd} does not take a text file; use .json or .yaml`);
56
- return { ...(loaded.frontmatter ?? {}), [spec.textField]: loaded.value };
58
+ const front = { ...(loaded.frontmatter ?? {}) };
59
+ if (frontmatter)
60
+ for (const key of registry_1.READ_ONLY_FRONTMATTER)
61
+ delete front[key];
62
+ return { ...front, [spec.textField]: loaded.value };
57
63
  }
58
64
  if (loaded.kind === 'array') {
59
65
  if (!spec.wrap)
@@ -103,6 +109,9 @@ async function runManageCommand(argv, readStdin) {
103
109
  }
104
110
  const { spec, rest } = match;
105
111
  const opts = parse(rest);
112
+ if (opts.frontmatter && !spec.optionalFrontmatterKeys) {
113
+ throw new errors_1.CliError(`--frontmatter only applies to metrio version get | create | update, not metrio ${spec.path.join(' ')}`);
114
+ }
106
115
  const client = new client_1.ConsoleClient(await (0, client_1.resolveConsoleAuth)());
107
116
  const tool = (await client.listTools()).find((t) => t.name === spec.tool);
108
117
  if (!tool) {
@@ -123,12 +132,18 @@ async function runManageCommand(argv, readStdin) {
123
132
  sources[key] = source;
124
133
  };
125
134
  if (opts.file)
126
- for (const [k, v] of Object.entries(fromFile(spec, opts.file, readStdin)))
135
+ for (const [k, v] of Object.entries(fromFile(spec, opts.file, opts.frontmatter, readStdin)))
127
136
  put(k, v, `file ${opts.file}`);
128
137
  if (opts.project && 'project_slug' in props)
129
138
  put('project_slug', opts.project, 'project');
130
139
  for (const [k, v] of Object.entries(values))
131
140
  put(k, v, `--${(0, schema_1.kebab)(k)}`);
141
+ // A version uses a tier OR a specific model, and the console lets tier_key win when both are
142
+ // sent; so a model chosen by flag must drop a tier that came from the file, or the flag is ignored.
143
+ if (('model_provider' in values || 'model_name' in values) && !('tier_key' in values) && 'tier_key' in args) {
144
+ delete args.tier_key;
145
+ delete sources.tier_key;
146
+ }
132
147
  // A prompt directory resolves against the final project_slug, so it waits for it.
133
148
  let promptDir;
134
149
  expected.forEach((arg, i) => {
@@ -163,11 +178,14 @@ async function runManageCommand(argv, readStdin) {
163
178
  throw new errors_1.CliError('Re-run with --yes to confirm.', 2);
164
179
  }
165
180
  const data = await client.callTool(spec.tool, accepted, { retry: tool.scope === 'read' });
166
- await (0, output_1.writeOutput)(data, {
181
+ const textOut = !!opts.outFile && /\.(md|markdown|txt)$/i.test(opts.outFile);
182
+ const withFrontmatter = opts.frontmatter && textOut && isRecord(data) && spec.frontmatterView;
183
+ await (0, output_1.writeOutput)(withFrontmatter ? spec.frontmatterView(data) : data, {
167
184
  json: opts.json,
168
185
  outFile: opts.outFile,
169
186
  textField: spec.textField,
170
- frontmatterKeys: spec.frontmatterKeys,
187
+ frontmatterKeys: spec.frontmatterKeys ?? (opts.frontmatter ? spec.optionalFrontmatterKeys : undefined),
188
+ frontmatterKeepNull: withFrontmatter ? spec.frontmatterKeepNull : undefined,
171
189
  downloadField: spec.downloadField,
172
190
  });
173
191
  if (isRecord(data) && data.valid === false) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metrio-ai/cli",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "packageManager": "yarn@4.14.1",
5
5
  "description": "Metrio AI command-line interface: run prompts and workflows, manage Metrio resources",
6
6
  "bin": {