@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.
- package/dist/agent/agentTypes.d.ts +21 -0
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +20 -17
- package/dist/agent/skills.d.ts +34 -4
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +126 -26
- package/dist/agent/trust.d.ts.map +1 -1
- package/dist/agent/trust.js +12 -0
- package/dist/commands/loader.d.ts.map +1 -1
- package/dist/commands/loader.js +2 -1
- package/dist/util/frontmatter.d.ts +66 -9
- package/dist/util/frontmatter.d.ts.map +1 -1
- package/dist/util/frontmatter.js +154 -15
- package/package.json +1 -1
|
@@ -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":"
|
|
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"}
|
package/dist/agent/agentTypes.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
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
|
package/dist/agent/skills.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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":"
|
|
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"}
|
package/dist/agent/skills.js
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
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(
|
|
161
|
+
const raw = fs.readFileSync(full, 'utf-8');
|
|
87
162
|
const { meta, body } = (0, frontmatter_1.parseFrontmatter)(raw);
|
|
88
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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":"
|
|
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"}
|
package/dist/agent/trust.js
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/commands/loader.js
CHANGED
|
@@ -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 =
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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
|
|
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"}
|
package/dist/util/frontmatter.js
CHANGED
|
@@ -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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
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)",
|