@nexrall/code-core 1.4.47 → 1.4.48

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.
@@ -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;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;AAiND;;;;;;;;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"}
@@ -43,6 +43,7 @@ const fs = __importStar(require("fs"));
43
43
  const path = __importStar(require("path"));
44
44
  const os = __importStar(require("os"));
45
45
  const index_1 = require("../plugins/index");
46
+ const frontmatter_1 = require("../util/frontmatter");
46
47
  const VALID_MEMORY_SCOPES = ['project', 'user', 'local'];
47
48
  function parseMemoryScope(v) {
48
49
  const s = (v ?? '').trim().toLowerCase();
@@ -395,18 +396,10 @@ const VALID_MODELS = [
395
396
  // Legacy tier aliases — still accepted, still resolved by the backend.
396
397
  'turbo', 'pro', 'ultra', 'fast',
397
398
  ];
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
- }
399
+ // parseFrontmatter now lives in ../util/frontmatter (shared with skills.ts and
400
+ // commands/loader.ts) so this file no longer carries its own copy — it used
401
+ // to, and the two silently drifted (the shared copy gained multi-line/nested
402
+ // value support during an audit; this one never did until this migration).
410
403
  /**
411
404
  * Parse the `model:` frontmatter of an agent definition.
412
405
  *
@@ -475,7 +468,7 @@ function loadDir(dir, source, into, warnings) {
475
468
  warnings.push({ file: full, agent: path.basename(entry.name, '.md'), message: `could not be read (${err.message}) — this agent was skipped` });
476
469
  continue;
477
470
  }
478
- const { meta, body, ok } = parseFrontmatter(raw);
471
+ const { meta, body, ok } = (0, frontmatter_1.parseFrontmatter)(raw);
479
472
  const name = (meta.name || path.basename(entry.name, '.md')).trim();
480
473
  if (!name)
481
474
  continue;
@@ -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":"AAyEA,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;AA8LD;;;;;;;;;;;;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,18 @@ 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
+ /** Frontmatter keys this parser understands, for typo/unrecognised-key detection (see SkillWarning). */
50
+ const KNOWN_META_KEYS = new Set([
51
+ 'name', 'description', 'model', 'mode',
52
+ 'disable-model-invocation', 'user-invocable',
53
+ 'license', 'compatibility', 'metadata', 'allowed-tools',
54
+ ]);
55
+ // agentskills.io's `name` constraint: 1-64 chars, lowercase alphanumeric and
56
+ // hyphens, no leading/trailing/consecutive hyphens. Validated LENIENTLY here
57
+ // (warn, still load) per the spec's own client-implementation guidance —
58
+ // rejecting a skill outright over a cosmetic name issue would be a worse
59
+ // failure mode than a slightly-off name that still works.
60
+ const SPEC_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
48
61
  function parseBool(v, fallback) {
49
62
  if (v === undefined)
50
63
  return fallback;
@@ -55,7 +68,14 @@ function parseBool(v, fallback) {
55
68
  return false;
56
69
  return fallback;
57
70
  }
58
- function toSkill(meta, body, name, source, dir) {
71
+ /** Space- (or comma-) separated tool list, same leniency as agentTypes.ts's parseToolList. */
72
+ function parseSpaceList(v) {
73
+ if (!v)
74
+ return undefined;
75
+ const items = v.split(/[,\s]+/).map((t) => t.trim()).filter(Boolean);
76
+ return items.length ? items : undefined;
77
+ }
78
+ function toSkill(meta, body, name, source, dir, nested) {
59
79
  const model = ['turbo', 'pro', 'ultra'].find((x) => x === (meta.model ?? '').toLowerCase());
60
80
  return {
61
81
  name,
@@ -67,13 +87,51 @@ function toSkill(meta, body, name, source, dir) {
67
87
  dir,
68
88
  disableModelInvocation: parseBool(meta['disable-model-invocation'], false),
69
89
  userInvocable: parseBool(meta['user-invocable'], true),
90
+ license: meta.license || undefined,
91
+ compatibility: meta.compatibility || undefined,
92
+ metadata: nested?.metadata,
93
+ allowedTools: parseSpaceList(meta['allowed-tools']),
70
94
  };
71
95
  }
96
+ /**
97
+ * Lenient validation warnings for a skill's frontmatter — never blocks loading, only reported.
98
+ *
99
+ * `rawName` is the name BEFORE lowercasing (whatever the frontmatter or the
100
+ * directory/file actually said) — validated against the spec's naming rule,
101
+ * which is itself case-sensitive ("lowercase ... only" is a real constraint,
102
+ * not cosmetic). `effectiveName` is the lowercased name used as the map key
103
+ * and `Skill.name` — using it here instead would make an uppercase name
104
+ * always "pass" validation simply because it had already been normalised
105
+ * away by the time this function saw it.
106
+ */
107
+ function validateSkillMeta(meta, rawName, effectiveName, dirName, file, warnings) {
108
+ if (!warnings)
109
+ return;
110
+ if (!SPEC_NAME_RE.test(rawName)) {
111
+ warnings.push({
112
+ file, name: effectiveName,
113
+ 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.`,
114
+ });
115
+ }
116
+ else if (rawName.length > 64) {
117
+ warnings.push({ file, name: effectiveName, message: `name is ${rawName.length} characters — the spec caps this at 64. Loaded anyway.` });
118
+ }
119
+ if (dirName !== undefined && meta.name && meta.name.trim().toLowerCase() !== dirName.toLowerCase()) {
120
+ warnings.push({
121
+ file, name: effectiveName,
122
+ 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.`,
123
+ });
124
+ }
125
+ const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
126
+ if (strayKeys.length) {
127
+ warnings.push({ file, name: effectiveName, message: `has unrecognised frontmatter key(s): ${strayKeys.join(', ')} — these are ignored.` });
128
+ }
129
+ }
72
130
  // Legacy flat-file skills: .nexrall/commands/<name>.md (and plugin commands/).
73
131
  // Kept as its own loader (rather than merged into loadSkillDir below) since the
74
132
  // naming/precedence rule ("skill directory wins over a same-name flat command")
75
133
  // requires knowing about both shapes before the caller merges them.
76
- function loadFlatCommandDir(dir, source, into) {
134
+ function loadFlatCommandDir(dir, source, into, warnings) {
77
135
  let files;
78
136
  try {
79
137
  files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
@@ -82,23 +140,27 @@ function loadFlatCommandDir(dir, source, into) {
82
140
  return;
83
141
  }
84
142
  for (const file of files) {
143
+ const full = path.join(dir, file);
85
144
  try {
86
- const raw = fs.readFileSync(path.join(dir, file), 'utf-8');
145
+ const raw = fs.readFileSync(full, 'utf-8');
87
146
  const { meta, body } = (0, frontmatter_1.parseFrontmatter)(raw);
88
- const name = (meta.name || path.basename(file, '.md')).trim().toLowerCase();
147
+ const rawName = (meta.name || path.basename(file, '.md')).trim();
148
+ const name = rawName.toLowerCase();
89
149
  if (!name)
90
150
  continue;
91
151
  if (source !== 'project' && into.has(name))
92
152
  continue; // earlier tiers win
153
+ validateSkillMeta(meta, rawName, name, undefined, full, warnings);
93
154
  into.set(name, toSkill(meta, body, name, source));
94
155
  }
95
- catch {
96
- /* skip malformed */
156
+ catch (err) {
157
+ warnings?.push({ file: full, name: path.basename(file, '.md'), message: `could not be read or parsed (${err.message}) — skipped.` });
97
158
  }
98
159
  }
99
160
  }
100
161
  // Directory-style skills: .nexrall/skills/<name>/SKILL.md (+ optional supporting files).
101
- function loadSkillDir(root, source, into) {
162
+ // Also used for the cross-client `.agents/skills/<name>/SKILL.md` convention — same shape.
163
+ function loadSkillDir(root, source, into, warnings) {
102
164
  let entries;
103
165
  try {
104
166
  entries = fs.readdirSync(root, { withFileTypes: true });
@@ -120,8 +182,9 @@ function loadSkillDir(root, source, into) {
120
182
  }
121
183
  try {
122
184
  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();
185
+ const { meta, body, nested } = (0, frontmatter_1.parseFrontmatter)(raw);
186
+ const rawName = (meta.name || entry.name).trim();
187
+ const name = rawName.toLowerCase();
125
188
  if (!name)
126
189
  continue;
127
190
  // A directory-style skill always wins over an earlier flat command of
@@ -129,10 +192,11 @@ function loadSkillDir(root, source, into) {
129
192
  // project tier always wins outright regardless of load order.
130
193
  if (source !== 'project' && into.has(name))
131
194
  continue;
132
- into.set(name, toSkill(meta, body, name, source, skillDir));
195
+ validateSkillMeta(meta, rawName, name, entry.name, skillFile, warnings);
196
+ into.set(name, toSkill(meta, body, name, source, skillDir, nested));
133
197
  }
134
- catch {
135
- /* skip malformed */
198
+ catch (err) {
199
+ warnings?.push({ file: skillFile, name: entry.name, message: `could not be read or parsed (${err.message}) — skipped.` });
136
200
  }
137
201
  }
138
202
  }
@@ -176,25 +240,45 @@ const BUILTIN_SKILLS = [
176
240
  ].join('\n'),
177
241
  },
178
242
  ];
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) {
243
+ /**
244
+ * Discover all skills, with diagnostics.
245
+ *
246
+ * Precedence, most to least authoritative: project `.nexrall/skills` >
247
+ * project `.nexrall/commands` > project `.agents/skills` (cross-client) >
248
+ * global `.nexrall/commands` > global `.nexrall/skills` > global
249
+ * `.agents/skills` (cross-client) > plugin > builtin.
250
+ *
251
+ * The `.agents/skills` scans are always the weakest source within their tier
252
+ * — see the module comment above for why. Directory-style
253
+ * `.nexrall/skills/<name>/SKILL.md` beats a flat `.nexrall/commands/<name>.md`
254
+ * of the same name within the same precedence tier.
255
+ */
256
+ function loadSkillsWithWarnings(workDir) {
183
257
  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);
258
+ const warnings = [];
259
+ // Project tier is last-write-wins (see loadSkillDir/loadFlatCommandDir's
260
+ // `source !== 'project'` guard), so the WEAKEST project source loads first.
261
+ loadSkillDir(path.join(workDir, '.agents', 'skills'), 'project', out, warnings);
262
+ loadFlatCommandDir(path.join(workDir, '.nexrall', 'commands'), 'project', out, warnings);
263
+ loadSkillDir(path.join(workDir, '.nexrall', 'skills'), 'project', out, warnings);
264
+ // Global tier is first-write-wins (the guard skips a repeat name), so the
265
+ // STRONGEST global source loads first, and .agents/skills loads last.
266
+ loadFlatCommandDir(path.join(os.homedir(), '.nexrall', 'commands'), 'global', out, warnings);
267
+ loadSkillDir(path.join(os.homedir(), '.nexrall', 'skills'), 'global', out, warnings);
268
+ loadSkillDir(path.join(os.homedir(), '.agents', 'skills'), 'global', out, warnings);
189
269
  for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'commands'))
190
- loadFlatCommandDir(dir, 'plugin', out);
270
+ loadFlatCommandDir(dir, 'plugin', out, warnings);
191
271
  for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'skills'))
192
- loadSkillDir(dir, 'plugin', out);
272
+ loadSkillDir(dir, 'plugin', out, warnings);
193
273
  for (const skill of BUILTIN_SKILLS) {
194
274
  if (!out.has(skill.name))
195
275
  out.set(skill.name, skill);
196
276
  }
197
- return [...out.values()];
277
+ return { skills: [...out.values()], warnings };
278
+ }
279
+ /** Discover all skills (project overrides global overrides plugin overrides builtin). See loadSkillsWithWarnings for the full precedence rule and diagnostics. */
280
+ function loadSkills(workDir) {
281
+ return loadSkillsWithWarnings(workDir).skills;
198
282
  }
199
283
  function findSkill(skills, name) {
200
284
  const want = name.replace(/^\//, '').trim().toLowerCase();
@@ -220,7 +304,7 @@ function summariseSkills(skills) {
220
304
  * substitution as slash commands — see commands/loader.ts#expandBody), then
221
305
  * prepend a short header pointing at the skill's own directory (if any) so
222
306
  * 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"
307
+ * on demand, mirroring the spec's "reference files from your SKILL.md"
224
308
  * guidance without loading them into context up front.
225
309
  */
226
310
  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,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.48",
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)",