@nexrall/code-core 1.4.47 → 1.4.49

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.
@@ -52,6 +52,10 @@ export interface AgentType {
52
52
  }
53
53
  /** Where a sub-agent's own persistent notes live. */
54
54
  export type AgentMemoryScope = 'project' | 'user' | 'local';
55
+ export declare const VALID_MODELS: readonly ["claude-sonnet-5", "claude-opus-5", "claude-fable-5", "gpt-5.4", "gpt-5.4-mini", "gpt-4.1", "turbo", "pro", "ultra", "fast"];
56
+ /** The four tier aliases specifically (a subset of VALID_MODELS) — the only values `AgentType.model`/`Skill.model` narrow to when recognised; a real model id passes through as a plain string instead. */
57
+ export declare const MODEL_TIER_ALIASES: readonly ["turbo", "pro", "ultra", "fast"];
58
+ export type ModelTier = (typeof MODEL_TIER_ALIASES)[number];
55
59
  /**
56
60
  * A problem found while loading an agent definition.
57
61
  *
@@ -66,6 +70,23 @@ export interface AgentWarning {
66
70
  agent: string;
67
71
  message: string;
68
72
  }
73
+ /**
74
+ * Parse the `model:` frontmatter of an agent definition.
75
+ *
76
+ * Accepts a tier alias (turbo/pro/ultra) OR a real model id now that the picker
77
+ * exposes those. Validation is deliberately NOT an allowlist of ids: this file
78
+ * ships in a client that users do not update in lockstep with the backend, so
79
+ * an allowlist here would reject a model that is perfectly valid server-side
80
+ * and make new models unusable until everyone upgrades. The backend already
81
+ * refuses anything it does not recognise (resolveExplicitModel), which is the
82
+ * correct place for that decision — it is the side that knows.
83
+ *
84
+ * Only aliases are lowercased. Real model ids are case- and punctuation-
85
+ * sensitive ('gpt-5.4', 'claude-haiku-4-5-20251001'), so lowercasing them
86
+ * blindly — as this function used to do to everything — would corrupt ids that
87
+ * happen to contain uppercase.
88
+ */
89
+ export declare function parseModel(v: string | undefined): string | undefined;
69
90
  /**
70
91
  * Discover all agent types. Precedence: project > global > plugin > builtin.
71
92
  *
@@ -1 +1 @@
1
- {"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAkCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qDAAqD;AACrD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AAmX5D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAuND;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAqB5G;AAED,yFAAyF;AACzF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,IAAI,SAAS,EAAE,CAE3C;AACD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAY1D;AASD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAIjG"}
1
+ {"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAmCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,qDAAqD;AACrD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;AAkX5D,eAAO,MAAM,YAAY,wIAKf,CAAC;AAEX,2MAA2M;AAC3M,eAAO,MAAM,kBAAkB,4CAA6C,CAAC;AAC7E,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAOD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMpE;AAoLD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAqB5G;AAED,yFAAyF;AACzF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,IAAI,SAAS,EAAE,CAE3C;AACD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAY1D;AASD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAIjG"}
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.MODEL_TIER_ALIASES = exports.VALID_MODELS = void 0;
37
+ exports.parseModel = parseModel;
36
38
  exports.loadAgentTypes = loadAgentTypes;
37
39
  exports.loadAgentTypesWithWarnings = loadAgentTypesWithWarnings;
38
40
  exports.knownToolNames = knownToolNames;
@@ -43,6 +45,7 @@ const fs = __importStar(require("fs"));
43
45
  const path = __importStar(require("path"));
44
46
  const os = __importStar(require("os"));
45
47
  const index_1 = require("../plugins/index");
48
+ const frontmatter_1 = require("../util/frontmatter");
46
49
  const VALID_MEMORY_SCOPES = ['project', 'user', 'local'];
47
50
  function parseMemoryScope(v) {
48
51
  const s = (v ?? '').trim().toLowerCase();
@@ -389,24 +392,24 @@ const KNOWN_META_KEYS = new Set([
389
392
  // `model: auto`, which parsed to undefined and was silently ignored, so the
390
393
  // agent ran on the session model while its file claimed otherwise. What made
391
394
  // that costly was the SILENCE, not the value.
392
- const VALID_MODELS = [
395
+ //
396
+ // Exported so skills.ts's `model:` field parses the SAME set — it used to
397
+ // carry its own, shorter copy (`turbo | pro | ultra`, missing `fast`) that
398
+ // silently dropped `model: fast` in a SKILL.md to `undefined` with no warning
399
+ // at all. Two independently-maintained model lists are exactly the kind of
400
+ // drift the shared parseFrontmatter migration was meant to end.
401
+ exports.VALID_MODELS = [
393
402
  'claude-sonnet-5', 'claude-opus-5', 'claude-fable-5',
394
403
  'gpt-5.4', 'gpt-5.4-mini', 'gpt-4.1',
395
404
  // Legacy tier aliases — still accepted, still resolved by the backend.
396
405
  'turbo', 'pro', 'ultra', 'fast',
397
406
  ];
398
- function parseFrontmatter(raw) {
399
- const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
400
- if (!m)
401
- return { meta: {}, body: raw.trim(), ok: false };
402
- const meta = {};
403
- for (const line of m[1].split(/\r?\n/)) {
404
- const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
405
- if (kv)
406
- meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
407
- }
408
- return { meta, body: (m[2] ?? '').trim(), ok: true };
409
- }
407
+ /** The four tier aliases specifically (a subset of VALID_MODELS) — the only values `AgentType.model`/`Skill.model` narrow to when recognised; a real model id passes through as a plain string instead. */
408
+ exports.MODEL_TIER_ALIASES = ['turbo', 'pro', 'ultra', 'fast'];
409
+ // parseFrontmatter now lives in ../util/frontmatter (shared with skills.ts and
410
+ // commands/loader.ts) so this file no longer carries its own copy — it used
411
+ // to, and the two silently drifted (the shared copy gained multi-line/nested
412
+ // value support during an audit; this one never did until this migration).
410
413
  /**
411
414
  * Parse the `model:` frontmatter of an agent definition.
412
415
  *
@@ -428,7 +431,7 @@ function parseModel(v) {
428
431
  if (!raw)
429
432
  return undefined;
430
433
  const lower = raw.toLowerCase();
431
- if (lower === 'turbo' || lower === 'pro' || lower === 'ultra' || lower === 'fast')
434
+ if (exports.MODEL_TIER_ALIASES.includes(lower))
432
435
  return lower;
433
436
  return raw;
434
437
  }
@@ -475,7 +478,7 @@ function loadDir(dir, source, into, warnings) {
475
478
  warnings.push({ file: full, agent: path.basename(entry.name, '.md'), message: `could not be read (${err.message}) — this agent was skipped` });
476
479
  continue;
477
480
  }
478
- const { meta, body, ok } = parseFrontmatter(raw);
481
+ const { meta, body, ok } = (0, frontmatter_1.parseFrontmatter)(raw);
479
482
  const name = (meta.name || path.basename(entry.name, '.md')).trim();
480
483
  if (!name)
481
484
  continue;
@@ -530,11 +533,11 @@ function loadDir(dir, source, into, warnings) {
530
533
  const declaredModel = meta.model === undefined ? undefined : parseModel(meta.model);
531
534
  if (meta.model !== undefined
532
535
  && (declaredModel === undefined
533
- || !VALID_MODELS.includes(declaredModel))) {
536
+ || !exports.VALID_MODELS.includes(declaredModel))) {
534
537
  warnings.push({
535
538
  file: full,
536
539
  agent: name,
537
- message: `has model: "${meta.model}", which this version does not recognise — expected one of ${VALID_MODELS.join(', ')}, or omit the line to inherit the current session's model. It will still be sent; the server decides whether it is valid.`,
540
+ message: `has model: "${meta.model}", which this version does not recognise — expected one of ${exports.VALID_MODELS.join(', ')}, or omit the line to inherit the current session's model. It will still be sent; the server decides whether it is valid.`,
538
541
  });
539
542
  }
540
543
  // The highest-value warning of the lot. An allowlist only ever GRANTS, so a
@@ -1,3 +1,10 @@
1
+ export interface SkillWarning {
2
+ /** Absolute path of the SKILL.md / command file the problem was found in. */
3
+ file: string;
4
+ /** Skill name, when one could be determined. */
5
+ name: string;
6
+ message: string;
7
+ }
1
8
  export interface Skill {
2
9
  name: string;
3
10
  description: string;
@@ -11,10 +18,33 @@ export interface Skill {
11
18
  disableModelInvocation: boolean;
12
19
  /** false → only the model may invoke it (auto-load only, not meant to be typed as a command). */
13
20
  userInvocable: boolean;
21
+ /** agentskills.io spec field — license name or reference to a bundled license file. Informational only. */
22
+ license?: string;
23
+ /** agentskills.io spec field — environment requirements (e.g. "Requires git, docker"). Informational only. */
24
+ compatibility?: string;
25
+ /** agentskills.io spec field — arbitrary author-supplied key/value metadata (from a nested `metadata:` block). */
26
+ metadata?: Record<string, string>;
27
+ /** agentskills.io spec field (experimental) — space-separated tool names the skill declares it needs. Not yet enforced as an allowlist; informational. */
28
+ allowedTools?: string[];
14
29
  }
15
- /** Discover all skills (project overrides global overrides plugin overrides builtin).
16
- * Directory-style `.nexrall/skills/<name>/SKILL.md` beats a flat `.nexrall/commands/<name>.md`
17
- * of the same name within the same precedence tier. */
30
+ /**
31
+ * Discover all skills, with diagnostics.
32
+ *
33
+ * Precedence, most to least authoritative: project `.nexrall/skills` >
34
+ * project `.nexrall/commands` > project `.agents/skills` (cross-client) >
35
+ * global `.nexrall/commands` > global `.nexrall/skills` > global
36
+ * `.agents/skills` (cross-client) > plugin > builtin.
37
+ *
38
+ * The `.agents/skills` scans are always the weakest source within their tier
39
+ * — see the module comment above for why. Directory-style
40
+ * `.nexrall/skills/<name>/SKILL.md` beats a flat `.nexrall/commands/<name>.md`
41
+ * of the same name within the same precedence tier.
42
+ */
43
+ export declare function loadSkillsWithWarnings(workDir: string): {
44
+ skills: Skill[];
45
+ warnings: SkillWarning[];
46
+ };
47
+ /** Discover all skills (project overrides global overrides plugin overrides builtin). See loadSkillsWithWarnings for the full precedence rule and diagnostics. */
18
48
  export declare function loadSkills(workDir: string): Skill[];
19
49
  export declare function findSkill(skills: Skill[], name: string): Skill | undefined;
20
50
  /** Skills the MODEL may auto-invoke via the use_skill tool (excludes disable-model-invocation:true ones). */
@@ -28,7 +58,7 @@ export declare function summariseSkills(skills: Skill[]): string;
28
58
  * substitution as slash commands — see commands/loader.ts#expandBody), then
29
59
  * prepend a short header pointing at the skill's own directory (if any) so
30
60
  * the model knows where to find supporting files it may need to read_file
31
- * on demand, mirroring Claude Code's "reference files from your SKILL.md"
61
+ * on demand, mirroring the spec's "reference files from your SKILL.md"
32
62
  * guidance without loading them into context up front.
33
63
  */
34
64
  export declare function expandSkill(skill: Skill, argString: string, workDir: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/agent/skills.ts"],"names":[],"mappings":"AA4CA,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD,0IAA0I;IAC1I,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kHAAkH;IAClH,sBAAsB,EAAE,OAAO,CAAC;IAChC,iGAAiG;IACjG,aAAa,EAAE,OAAO,CAAC;CACxB;AA8HD;;wDAEwD;AACxD,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,CAanD;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAG1E;AAED,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAE5D;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAE5D;AAED,oHAAoH;AACpH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAIvD;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAcpF"}
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/agent/skills.ts"],"names":[],"mappings":"AA0EA,MAAM,WAAW,YAAY;IAC3B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD,0IAA0I;IAC1I,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kHAAkH;IAClH,sBAAsB,EAAE,OAAO,CAAC;IAChC,iGAAiG;IACjG,aAAa,EAAE,OAAO,CAAC;IACvB,2GAA2G;IAC3G,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8GAA8G;IAC9G,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kHAAkH;IAClH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,0JAA0J;IAC1J,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AA6MD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAsBrG;AAED,kKAAkK;AAClK,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,CAEnD;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAG1E;AAED,6GAA6G;AAC7G,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAE5D;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAE5D;AAED,oHAAoH;AACpH,wBAAgB,eAAe,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAIvD;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAcpF"}
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadSkillsWithWarnings = loadSkillsWithWarnings;
36
37
  exports.loadSkills = loadSkills;
37
38
  exports.findSkill = findSkill;
38
39
  exports.autoInvokableSkills = autoInvokableSkills;
@@ -45,6 +46,19 @@ const os = __importStar(require("os"));
45
46
  const index_1 = require("../plugins/index");
46
47
  const loader_1 = require("../commands/loader");
47
48
  const frontmatter_1 = require("../util/frontmatter");
49
+ const agentTypes_1 = require("./agentTypes");
50
+ /** Frontmatter keys this parser understands, for typo/unrecognised-key detection (see SkillWarning). */
51
+ const KNOWN_META_KEYS = new Set([
52
+ 'name', 'description', 'model', 'mode',
53
+ 'disable-model-invocation', 'user-invocable',
54
+ 'license', 'compatibility', 'metadata', 'allowed-tools',
55
+ ]);
56
+ // agentskills.io's `name` constraint: 1-64 chars, lowercase alphanumeric and
57
+ // hyphens, no leading/trailing/consecutive hyphens. Validated LENIENTLY here
58
+ // (warn, still load) per the spec's own client-implementation guidance —
59
+ // rejecting a skill outright over a cosmetic name issue would be a worse
60
+ // failure mode than a slightly-off name that still works.
61
+ const SPEC_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
48
62
  function parseBool(v, fallback) {
49
63
  if (v === undefined)
50
64
  return fallback;
@@ -55,8 +69,15 @@ function parseBool(v, fallback) {
55
69
  return false;
56
70
  return fallback;
57
71
  }
58
- function toSkill(meta, body, name, source, dir) {
59
- const model = ['turbo', 'pro', 'ultra'].find((x) => x === (meta.model ?? '').toLowerCase());
72
+ /** Space- (or comma-) separated tool list, same leniency as agentTypes.ts's parseToolList. */
73
+ function parseSpaceList(v) {
74
+ if (!v)
75
+ return undefined;
76
+ const items = v.split(/[,\s]+/).map((t) => t.trim()).filter(Boolean);
77
+ return items.length ? items : undefined;
78
+ }
79
+ function toSkill(meta, body, name, source, dir, nested) {
80
+ const model = (0, agentTypes_1.parseModel)(meta.model);
60
81
  return {
61
82
  name,
62
83
  description: meta.description || `Custom /${name} skill`,
@@ -67,13 +88,66 @@ function toSkill(meta, body, name, source, dir) {
67
88
  dir,
68
89
  disableModelInvocation: parseBool(meta['disable-model-invocation'], false),
69
90
  userInvocable: parseBool(meta['user-invocable'], true),
91
+ license: meta.license || undefined,
92
+ compatibility: meta.compatibility || undefined,
93
+ metadata: nested?.metadata,
94
+ allowedTools: parseSpaceList(meta['allowed-tools']),
70
95
  };
71
96
  }
97
+ /**
98
+ * Lenient validation warnings for a skill's frontmatter — never blocks loading, only reported.
99
+ *
100
+ * `rawName` is the name BEFORE lowercasing (whatever the frontmatter or the
101
+ * directory/file actually said) — validated against the spec's naming rule,
102
+ * which is itself case-sensitive ("lowercase ... only" is a real constraint,
103
+ * not cosmetic). `effectiveName` is the lowercased name used as the map key
104
+ * and `Skill.name` — using it here instead would make an uppercase name
105
+ * always "pass" validation simply because it had already been normalised
106
+ * away by the time this function saw it.
107
+ */
108
+ function validateSkillMeta(meta, rawName, effectiveName, dirName, file, warnings) {
109
+ if (!warnings)
110
+ return;
111
+ if (!SPEC_NAME_RE.test(rawName)) {
112
+ warnings.push({
113
+ file, name: effectiveName,
114
+ message: `name "${rawName}" does not match the agentskills.io convention (lowercase letters, numbers, and single hyphens only) — loaded anyway, but other clients reading this skill may warn or reject it.`,
115
+ });
116
+ }
117
+ else if (rawName.length > 64) {
118
+ warnings.push({ file, name: effectiveName, message: `name is ${rawName.length} characters — the spec caps this at 64. Loaded anyway.` });
119
+ }
120
+ if (dirName !== undefined && meta.name && meta.name.trim().toLowerCase() !== dirName.toLowerCase()) {
121
+ warnings.push({
122
+ file, name: effectiveName,
123
+ message: `frontmatter name "${meta.name}" does not match its directory name "${dirName}" — the spec requires them to match for cross-client compatibility. Loaded anyway, using the frontmatter name.`,
124
+ });
125
+ }
126
+ const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
127
+ if (strayKeys.length) {
128
+ warnings.push({ file, name: effectiveName, message: `has unrecognised frontmatter key(s): ${strayKeys.join(', ')} — these are ignored.` });
129
+ }
130
+ // Same class of mistake agentTypes.ts already guards against for sub-agents:
131
+ // pass the value through regardless (parseModel never rejects), but say
132
+ // something when this build doesn't recognise it, so a typo doesn't look
133
+ // identical to "inherit the session's model". Before this, `model: fast`
134
+ // (a valid tier alias) silently parsed to undefined in a skill because this
135
+ // file carried its own, shorter, independently-drifted copy of the model list.
136
+ const declaredModel = meta.model === undefined ? undefined : (0, agentTypes_1.parseModel)(meta.model);
137
+ if (meta.model !== undefined
138
+ && (declaredModel === undefined
139
+ || !agentTypes_1.VALID_MODELS.includes(declaredModel))) {
140
+ warnings.push({
141
+ file, name: effectiveName,
142
+ message: `has model: "${meta.model}", which this version does not recognise — expected one of ${agentTypes_1.VALID_MODELS.join(', ')}, or omit the line to inherit the current session's model. It will still be sent; the server decides whether it is valid.`,
143
+ });
144
+ }
145
+ }
72
146
  // Legacy flat-file skills: .nexrall/commands/<name>.md (and plugin commands/).
73
147
  // Kept as its own loader (rather than merged into loadSkillDir below) since the
74
148
  // naming/precedence rule ("skill directory wins over a same-name flat command")
75
149
  // requires knowing about both shapes before the caller merges them.
76
- function loadFlatCommandDir(dir, source, into) {
150
+ function loadFlatCommandDir(dir, source, into, warnings) {
77
151
  let files;
78
152
  try {
79
153
  files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
@@ -82,23 +156,27 @@ function loadFlatCommandDir(dir, source, into) {
82
156
  return;
83
157
  }
84
158
  for (const file of files) {
159
+ const full = path.join(dir, file);
85
160
  try {
86
- const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
161
+ const raw = fs.readFileSync(full, 'utf-8');
87
162
  const { meta, body } = (0, frontmatter_1.parseFrontmatter)(raw);
88
- const name = (meta.name || path.basename(file, '.md')).trim().toLowerCase();
163
+ const rawName = (meta.name || path.basename(file, '.md')).trim();
164
+ const name = rawName.toLowerCase();
89
165
  if (!name)
90
166
  continue;
91
167
  if (source !== 'project' && into.has(name))
92
168
  continue; // earlier tiers win
169
+ validateSkillMeta(meta, rawName, name, undefined, full, warnings);
93
170
  into.set(name, toSkill(meta, body, name, source));
94
171
  }
95
- catch {
96
- /* skip malformed */
172
+ catch (err) {
173
+ warnings?.push({ file: full, name: path.basename(file, '.md'), message: `could not be read or parsed (${err.message}) — skipped.` });
97
174
  }
98
175
  }
99
176
  }
100
177
  // Directory-style skills: .nexrall/skills/<name>/SKILL.md (+ optional supporting files).
101
- function loadSkillDir(root, source, into) {
178
+ // Also used for the cross-client `.agents/skills/<name>/SKILL.md` convention — same shape.
179
+ function loadSkillDir(root, source, into, warnings) {
102
180
  let entries;
103
181
  try {
104
182
  entries = fs.readdirSync(root, { withFileTypes: true });
@@ -120,8 +198,9 @@ function loadSkillDir(root, source, into) {
120
198
  }
121
199
  try {
122
200
  const raw = fs.readFileSync(skillFile, 'utf-8');
123
- const { meta, body } = (0, frontmatter_1.parseFrontmatter)(raw);
124
- const name = (meta.name || entry.name).trim().toLowerCase();
201
+ const { meta, body, nested } = (0, frontmatter_1.parseFrontmatter)(raw);
202
+ const rawName = (meta.name || entry.name).trim();
203
+ const name = rawName.toLowerCase();
125
204
  if (!name)
126
205
  continue;
127
206
  // A directory-style skill always wins over an earlier flat command of
@@ -129,10 +208,11 @@ function loadSkillDir(root, source, into) {
129
208
  // project tier always wins outright regardless of load order.
130
209
  if (source !== 'project' && into.has(name))
131
210
  continue;
132
- into.set(name, toSkill(meta, body, name, source, skillDir));
211
+ validateSkillMeta(meta, rawName, name, entry.name, skillFile, warnings);
212
+ into.set(name, toSkill(meta, body, name, source, skillDir, nested));
133
213
  }
134
- catch {
135
- /* skip malformed */
214
+ catch (err) {
215
+ warnings?.push({ file: skillFile, name: entry.name, message: `could not be read or parsed (${err.message}) — skipped.` });
136
216
  }
137
217
  }
138
218
  }
@@ -176,25 +256,45 @@ const BUILTIN_SKILLS = [
176
256
  ].join('\n'),
177
257
  },
178
258
  ];
179
- /** Discover all skills (project overrides global overrides plugin overrides builtin).
180
- * Directory-style `.nexrall/skills/<name>/SKILL.md` beats a flat `.nexrall/commands/<name>.md`
181
- * of the same name within the same precedence tier. */
182
- function loadSkills(workDir) {
259
+ /**
260
+ * Discover all skills, with diagnostics.
261
+ *
262
+ * Precedence, most to least authoritative: project `.nexrall/skills` >
263
+ * project `.nexrall/commands` > project `.agents/skills` (cross-client) >
264
+ * global `.nexrall/commands` > global `.nexrall/skills` > global
265
+ * `.agents/skills` (cross-client) > plugin > builtin.
266
+ *
267
+ * The `.agents/skills` scans are always the weakest source within their tier
268
+ * — see the module comment above for why. Directory-style
269
+ * `.nexrall/skills/<name>/SKILL.md` beats a flat `.nexrall/commands/<name>.md`
270
+ * of the same name within the same precedence tier.
271
+ */
272
+ function loadSkillsWithWarnings(workDir) {
183
273
  const out = new Map();
184
- // Flat commands first (so a same-tier skill directory below can override them).
185
- loadFlatCommandDir(path.join(workDir, '.nexrall', 'commands'), 'project', out);
186
- loadSkillDir(path.join(workDir, '.nexrall', 'skills'), 'project', out);
187
- loadFlatCommandDir(path.join(os.homedir(), '.nexrall', 'commands'), 'global', out);
188
- loadSkillDir(path.join(os.homedir(), '.nexrall', 'skills'), 'global', out);
274
+ const warnings = [];
275
+ // Project tier is last-write-wins (see loadSkillDir/loadFlatCommandDir's
276
+ // `source !== 'project'` guard), so the WEAKEST project source loads first.
277
+ loadSkillDir(path.join(workDir, '.agents', 'skills'), 'project', out, warnings);
278
+ loadFlatCommandDir(path.join(workDir, '.nexrall', 'commands'), 'project', out, warnings);
279
+ loadSkillDir(path.join(workDir, '.nexrall', 'skills'), 'project', out, warnings);
280
+ // Global tier is first-write-wins (the guard skips a repeat name), so the
281
+ // STRONGEST global source loads first, and .agents/skills loads last.
282
+ loadFlatCommandDir(path.join(os.homedir(), '.nexrall', 'commands'), 'global', out, warnings);
283
+ loadSkillDir(path.join(os.homedir(), '.nexrall', 'skills'), 'global', out, warnings);
284
+ loadSkillDir(path.join(os.homedir(), '.agents', 'skills'), 'global', out, warnings);
189
285
  for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'commands'))
190
- loadFlatCommandDir(dir, 'plugin', out);
286
+ loadFlatCommandDir(dir, 'plugin', out, warnings);
191
287
  for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'skills'))
192
- loadSkillDir(dir, 'plugin', out);
288
+ loadSkillDir(dir, 'plugin', out, warnings);
193
289
  for (const skill of BUILTIN_SKILLS) {
194
290
  if (!out.has(skill.name))
195
291
  out.set(skill.name, skill);
196
292
  }
197
- return [...out.values()];
293
+ return { skills: [...out.values()], warnings };
294
+ }
295
+ /** Discover all skills (project overrides global overrides plugin overrides builtin). See loadSkillsWithWarnings for the full precedence rule and diagnostics. */
296
+ function loadSkills(workDir) {
297
+ return loadSkillsWithWarnings(workDir).skills;
198
298
  }
199
299
  function findSkill(skills, name) {
200
300
  const want = name.replace(/^\//, '').trim().toLowerCase();
@@ -220,7 +320,7 @@ function summariseSkills(skills) {
220
320
  * substitution as slash commands — see commands/loader.ts#expandBody), then
221
321
  * prepend a short header pointing at the skill's own directory (if any) so
222
322
  * the model knows where to find supporting files it may need to read_file
223
- * on demand, mirroring Claude Code's "reference files from your SKILL.md"
323
+ * on demand, mirroring the spec's "reference files from your SKILL.md"
224
324
  * guidance without loading them into context up front.
225
325
  */
226
326
  function expandSkill(skill, argString, workDir) {
@@ -1 +1 @@
1
- {"version":3,"file":"trust.d.ts","sourceRoot":"","sources":["../../src/agent/trust.ts"],"names":[],"mappings":"AAiDA;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CA0DxD"}
1
+ {"version":3,"file":"trust.d.ts","sourceRoot":"","sources":["../../src/agent/trust.ts"],"names":[],"mappings":"AAqDA;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAkExD"}
@@ -54,6 +54,10 @@ const path = __importStar(require("path"));
54
54
  // • `.nexrall/{skills,commands}` define playbooks the agent will follow,
55
55
  // and `.nexrall/permissions.json` can pre-approve tool calls that would
56
56
  // otherwise prompt.
57
+ // • `.agents/skills/` is the same kind of playbook, discovered from the
58
+ // agentskills.io cross-client convention (see agent/skills.ts) rather
59
+ // than a Nexrall-specific path — worth its own line since it may have
60
+ // been authored for a different client entirely.
57
61
  //
58
62
  // So `git clone` of a hostile repo followed by pointing an agent at it is
59
63
  // enough to execute attacker-chosen commands before a human reviews anything.
@@ -146,6 +150,14 @@ function detectTrustSignals(dir) {
146
150
  if (exists('.nexrall', 'agents')) {
147
151
  signals.push('.nexrall/agents/ — custom sub-agent definitions');
148
152
  }
153
+ if (exists('.agents', 'skills')) {
154
+ // Same risk class as .nexrall/skills/ (a playbook is instructions the
155
+ // agent follows, not code it executes) — called out separately because
156
+ // this path is the agentskills.io cross-client convention, so a skill
157
+ // here may have been authored for/by a DIFFERENT tool than the one
158
+ // asking to trust this folder right now.
159
+ signals.push('.agents/skills/ — cross-client agent playbooks (agentskills.io convention)');
160
+ }
149
161
  return signals;
150
162
  }
151
163
  //# sourceMappingURL=trust.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/commands/loader.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;CACrD;AAsED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CASjE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAG7F;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAuCnF;AAED,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE3F"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/commands/loader.ts"],"names":[],"mappings":"AA+BA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;CACrD;AAsED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,CASjE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAG7F;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAuCnF;AAED,4FAA4F;AAC5F,wBAAgB,aAAa,CAAC,GAAG,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE3F"}
@@ -43,6 +43,7 @@ const os = __importStar(require("os"));
43
43
  const child_process_1 = require("child_process");
44
44
  const index_1 = require("../plugins/index");
45
45
  const frontmatter_1 = require("../util/frontmatter");
46
+ const agentTypes_1 = require("../agent/agentTypes");
46
47
  // ── Built-in commands ─────────────────────────────────────────────────────────
47
48
  // Shipped with the product; lowest precedence (project > global > builtin), so
48
49
  // a user can override any of them by creating a file with the same name.
@@ -97,7 +98,7 @@ function loadDir(dir, source, into) {
97
98
  continue;
98
99
  if (source !== 'project' && into.has(name))
99
100
  continue; // earlier tiers win
100
- const model = ['turbo', 'pro', 'ultra'].find((x) => x === (meta.model ?? '').toLowerCase());
101
+ const model = (0, agentTypes_1.parseModel)(meta.model);
101
102
  into.set(name, {
102
103
  name,
103
104
  description: meta.description || `Custom /${name} command`,
@@ -1,22 +1,79 @@
1
1
  /**
2
2
  * Shared YAML-ish frontmatter parser for the markdown-based config formats
3
- * this project uses (slash commands, skills, agent definitions). All three
4
- * previously carried their own byte-for-byte identical copy of this function
5
- * extracted here so a future fix (e.g. multi-line values, list syntax)
6
- * only has to happen once and cannot silently drift between them.
3
+ * this project uses (slash commands, skills, agent definitions, plugin
4
+ * manifests). Extracted here so a fix only has to happen once and cannot
5
+ * silently drift between the formats that consume it.
7
6
  *
8
7
  * ---
9
8
  * key: value
10
9
  * another-key: "quoted value"
10
+ * description: >-
11
+ * a long value that folds onto
12
+ * the following indented lines
13
+ * metadata:
14
+ * author: example-org
15
+ * version: "1.0"
11
16
  * ---
12
17
  * body text...
13
18
  *
14
- * Deliberately minimal: single-line `key: value` pairs only, no nested
15
- * structures or YAML lists matches what commands/loader.ts, agent/skills.ts
16
- * and agent/agentTypes.ts have always accepted.
19
+ * Deliberately still not a full YAML parser (no lists, no multi-level
20
+ * nesting, no anchors)but it now handles the three shapes real-world
21
+ * SKILL.md files (including the `agentskills.io` spec's own examples) use:
22
+ *
23
+ * 1. Plain multi-line values: a `key: value` line followed by indented
24
+ * lines with no `sub: value` shape of their own are folded onto the
25
+ * value with a single space, the same way a human reads wrapped prose.
26
+ * 2. Block scalars: `key: >`, `>-`, `|`, `|-` followed by indented lines.
27
+ * `>` folds lines into a single space-joined string (YAML "folded"
28
+ * style); `|` keeps them literal, newline-joined ("literal" style).
29
+ * The `-` chomping indicator is accepted for compatibility but every
30
+ * value returned here is trimmed regardless, since these are metadata
31
+ * strings, not file content whose trailing newline matters.
32
+ * 3. One-level nested maps: `key:` with NOTHING after the colon, followed
33
+ * by indented `sub: value` lines, becomes a nested map — returned
34
+ * separately in `nested[key]`, NEVER merged into the flat `meta` object.
35
+ *
36
+ * That last rule is load-bearing, not cosmetic. The previous version of
37
+ * this parser treated every trimmed line as a flat top-level key regardless
38
+ * of indentation, so a block like
39
+ *
40
+ * tools: read_file, search_files, glob
41
+ * metadata:
42
+ * tools: read_file, search_files, glob, bash, write_file, delete_file
43
+ *
44
+ * silently let the indented `tools:` inside `metadata:` OVERWRITE the real,
45
+ * innocent-looking top-level `tools:` a reviewer would actually read —
46
+ * because both lines matched the same regex and the second write won. That
47
+ * is a privilege-escalation bug in an agent-definition file, not a display
48
+ * quirk: `agentTypes.ts`'s tool allowlist reads `meta.tools` and would have
49
+ * granted `bash`/`write_file`/`delete_file` from a definition that visibly
50
+ * only requests three read-only tools. Every indented line now belongs to
51
+ * ITS OWN key's continuation/nested-map, never to the flat top-level
52
+ * namespace, which makes this class of collision structurally impossible
53
+ * rather than merely untested.
17
54
  */
18
- export declare function parseFrontmatter(raw: string): {
55
+ export interface ParsedFrontmatter {
56
+ /** Flat top-level `key: value` pairs. Indented/nested content never leaks in here. */
19
57
  meta: Record<string, string>;
58
+ /** Markdown body after the closing `---`. */
20
59
  body: string;
21
- };
60
+ /**
61
+ * One-level nested maps, keyed by their parent field name — e.g. a
62
+ * `metadata:` block with indented `author: ...` / `version: ...` lines
63
+ * becomes `nested.metadata = { author: '...', version: '...' }`.
64
+ * Absent (not an empty object) when no field had a nested map, so
65
+ * callers that don't care can ignore this without an extra check.
66
+ */
67
+ nested?: Record<string, Record<string, string>>;
68
+ /**
69
+ * True if a valid `---`-delimited frontmatter block was found at all. False
70
+ * means the whole file is `body` and `meta`/`nested` are empty — callers
71
+ * that treat "no parseable frontmatter" as a distinct, fail-closed case
72
+ * (e.g. agent definitions default to read-only rather than unrestricted
73
+ * when this is false) should check this instead of `Object.keys(meta).length`,
74
+ * which can't tell "no frontmatter" apart from "frontmatter with no fields".
75
+ */
76
+ ok: boolean;
77
+ }
78
+ export declare function parseFrontmatter(raw: string): ParsedFrontmatter;
22
79
  //# sourceMappingURL=frontmatter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAS5F"}
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/util/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,MAAM,WAAW,iBAAiB;IAChC,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,EAAE,EAAE,OAAO,CAAC;CACb;AAcD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAqG/D"}
@@ -1,33 +1,172 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseFrontmatter = parseFrontmatter;
4
2
  /**
5
3
  * Shared YAML-ish frontmatter parser for the markdown-based config formats
6
- * this project uses (slash commands, skills, agent definitions). All three
7
- * previously carried their own byte-for-byte identical copy of this function
8
- * extracted here so a future fix (e.g. multi-line values, list syntax)
9
- * only has to happen once and cannot silently drift between them.
4
+ * this project uses (slash commands, skills, agent definitions, plugin
5
+ * manifests). Extracted here so a fix only has to happen once and cannot
6
+ * silently drift between the formats that consume it.
10
7
  *
11
8
  * ---
12
9
  * key: value
13
10
  * another-key: "quoted value"
11
+ * description: >-
12
+ * a long value that folds onto
13
+ * the following indented lines
14
+ * metadata:
15
+ * author: example-org
16
+ * version: "1.0"
14
17
  * ---
15
18
  * body text...
16
19
  *
17
- * Deliberately minimal: single-line `key: value` pairs only, no nested
18
- * structures or YAML lists matches what commands/loader.ts, agent/skills.ts
19
- * and agent/agentTypes.ts have always accepted.
20
+ * Deliberately still not a full YAML parser (no lists, no multi-level
21
+ * nesting, no anchors)but it now handles the three shapes real-world
22
+ * SKILL.md files (including the `agentskills.io` spec's own examples) use:
23
+ *
24
+ * 1. Plain multi-line values: a `key: value` line followed by indented
25
+ * lines with no `sub: value` shape of their own are folded onto the
26
+ * value with a single space, the same way a human reads wrapped prose.
27
+ * 2. Block scalars: `key: >`, `>-`, `|`, `|-` followed by indented lines.
28
+ * `>` folds lines into a single space-joined string (YAML "folded"
29
+ * style); `|` keeps them literal, newline-joined ("literal" style).
30
+ * The `-` chomping indicator is accepted for compatibility but every
31
+ * value returned here is trimmed regardless, since these are metadata
32
+ * strings, not file content whose trailing newline matters.
33
+ * 3. One-level nested maps: `key:` with NOTHING after the colon, followed
34
+ * by indented `sub: value` lines, becomes a nested map — returned
35
+ * separately in `nested[key]`, NEVER merged into the flat `meta` object.
36
+ *
37
+ * That last rule is load-bearing, not cosmetic. The previous version of
38
+ * this parser treated every trimmed line as a flat top-level key regardless
39
+ * of indentation, so a block like
40
+ *
41
+ * tools: read_file, search_files, glob
42
+ * metadata:
43
+ * tools: read_file, search_files, glob, bash, write_file, delete_file
44
+ *
45
+ * silently let the indented `tools:` inside `metadata:` OVERWRITE the real,
46
+ * innocent-looking top-level `tools:` a reviewer would actually read —
47
+ * because both lines matched the same regex and the second write won. That
48
+ * is a privilege-escalation bug in an agent-definition file, not a display
49
+ * quirk: `agentTypes.ts`'s tool allowlist reads `meta.tools` and would have
50
+ * granted `bash`/`write_file`/`delete_file` from a definition that visibly
51
+ * only requests three read-only tools. Every indented line now belongs to
52
+ * ITS OWN key's continuation/nested-map, never to the flat top-level
53
+ * namespace, which makes this class of collision structurally impossible
54
+ * rather than merely untested.
20
55
  */
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.parseFrontmatter = parseFrontmatter;
58
+ const TOP_LEVEL_KEY = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/;
59
+ const BLOCK_SCALAR = /^[|>][+-]?$/;
60
+ function leadingIndent(line) {
61
+ const m = /^[ \t]*/.exec(line);
62
+ return m ? m[0].length : 0;
63
+ }
64
+ function unquote(v) {
65
+ return v.trim().replace(/^["']|["']$/g, '');
66
+ }
21
67
  function parseFrontmatter(raw) {
22
68
  const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
23
69
  if (!m)
24
- return { meta: {}, body: raw.trim() };
70
+ return { meta: {}, body: raw.trim(), ok: false };
71
+ const lines = m[1].split(/\r?\n/);
25
72
  const meta = {};
26
- for (const line of m[1].split(/\r?\n/)) {
27
- const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
28
- if (kv)
29
- meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
73
+ let nested;
74
+ let i = 0;
75
+ while (i < lines.length) {
76
+ const line = lines[i];
77
+ // Blank lines and stray indented lines with no top-level key before them
78
+ // (malformed input) are simply skipped — lenient parsing, same spirit as
79
+ // the rest of this file: warn elsewhere, don't throw here.
80
+ if (!line.trim() || leadingIndent(line) > 0) {
81
+ i++;
82
+ continue;
83
+ }
84
+ const kv = TOP_LEVEL_KEY.exec(line.trim());
85
+ if (!kv) {
86
+ i++;
87
+ continue;
88
+ }
89
+ const key = kv[1].toLowerCase();
90
+ const value = kv[2].trim();
91
+ // Collect every immediately-following indented line as this key's
92
+ // continuation block, whatever shape it turns out to be.
93
+ const continuation = [];
94
+ let j = i + 1;
95
+ while (j < lines.length && (leadingIndent(lines[j]) > 0 || !lines[j].trim())) {
96
+ continuation.push(lines[j]);
97
+ j++;
98
+ }
99
+ // Trim trailing blank continuation lines (keeps indent-detection below
100
+ // simple and doesn't affect folded/literal output).
101
+ while (continuation.length && !continuation[continuation.length - 1].trim())
102
+ continuation.pop();
103
+ if (BLOCK_SCALAR.test(value)) {
104
+ // `key: >`, `>-`, `|`, `|-` — content is EVERY continuation line.
105
+ // `>` = YAML "folded" style (join into one space-separated line);
106
+ // `|` = YAML "literal" style (keep the line breaks as-is).
107
+ const folded = value.startsWith('>');
108
+ const base = continuation.length ? leadingIndent(continuation.find((l) => l.trim()) ?? continuation[0]) : 0;
109
+ const dedented = continuation.map((l) => (l.trim() ? l.slice(Math.min(base, leadingIndent(l))) : ''));
110
+ meta[key] = folded
111
+ ? dedented.join(' ').replace(/\s+/g, ' ').trim() // folded (>) style
112
+ : dedented.join('\n').trim(); // literal (|) style
113
+ }
114
+ else if (value === '') {
115
+ // `key:` with nothing after it. Two possibilities:
116
+ // - continuation lines look like `sub: value` → this is a nested map,
117
+ // isolated in `nested[key]` and NEVER merged into flat `meta`.
118
+ // - continuation lines are plain prose → fold them as the value.
119
+ const looksNested = continuation.length > 0 && continuation.every((l) => !l.trim() || TOP_LEVEL_KEY.test(l.trim()));
120
+ if (looksNested && continuation.some((l) => l.trim())) {
121
+ const map = {};
122
+ for (const l of continuation) {
123
+ const sub = TOP_LEVEL_KEY.exec(l.trim());
124
+ if (sub)
125
+ map[sub[1].toLowerCase()] = unquote(sub[2]);
126
+ }
127
+ nested = nested ?? {};
128
+ nested[key] = map;
129
+ // Deliberately no `meta[key] = ...` here — an empty-header field with
130
+ // a nested map has no flat scalar value of its own.
131
+ }
132
+ else if (continuation.length) {
133
+ meta[key] = [value, ...continuation.map((l) => l.trim())].filter(Boolean).join(' ').trim();
134
+ }
135
+ else {
136
+ meta[key] = '';
137
+ }
138
+ }
139
+ else {
140
+ // Normal `key: value`. Fold any plain-prose continuation lines onto it
141
+ // (the common case: a long `description:` wrapped across lines) —
142
+ // but only if none of them look like their own `sub: value` pair,
143
+ // which would instead mean the author meant something structured and
144
+ // folding it into one string would corrupt it silently.
145
+ const looksNested = continuation.length > 0 && continuation.every((l) => !l.trim() || TOP_LEVEL_KEY.test(l.trim()));
146
+ if (continuation.length && !looksNested) {
147
+ meta[key] = unquote([value, ...continuation.map((l) => l.trim())].join(' '));
148
+ }
149
+ else if (continuation.length && looksNested) {
150
+ // `key: value` immediately followed by what looks like a nested map —
151
+ // ambiguous/malformed YAML. Keep the scalar value (least surprising:
152
+ // it's what a naive line-by-line reader sees first) and surface the
153
+ // rest as a nested map too, rather than silently dropping either.
154
+ meta[key] = unquote(value);
155
+ const map = {};
156
+ for (const l of continuation) {
157
+ const sub = TOP_LEVEL_KEY.exec(l.trim());
158
+ if (sub)
159
+ map[sub[1].toLowerCase()] = unquote(sub[2]);
160
+ }
161
+ nested = nested ?? {};
162
+ nested[key] = map;
163
+ }
164
+ else {
165
+ meta[key] = unquote(value);
166
+ }
167
+ }
168
+ i = j;
30
169
  }
31
- return { meta, body: (m[2] ?? '').trim() };
170
+ return { meta, body: (m[2] ?? '').trim(), nested, ok: true };
32
171
  }
33
172
  //# sourceMappingURL=frontmatter.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexrall/code-core",
3
- "version": "1.4.47",
3
+ "version": "1.4.49",
4
4
  "description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
5
5
  "license": "MIT",
6
6
  "author": "Nexrall <support@nexrall.com> (https://nexrall.com)",