@cleocode/cant 2026.3.76
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/LICENSE +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +18 -0
- package/dist/migrate/converter.d.ts +21 -0
- package/dist/migrate/converter.js +390 -0
- package/dist/migrate/diff.d.ts +30 -0
- package/dist/migrate/diff.js +107 -0
- package/dist/migrate/index.d.ts +22 -0
- package/dist/migrate/index.js +28 -0
- package/dist/migrate/markdown-parser.d.ts +79 -0
- package/dist/migrate/markdown-parser.js +307 -0
- package/dist/migrate/serializer.d.ts +81 -0
- package/dist/migrate/serializer.js +155 -0
- package/dist/migrate/types.d.ts +90 -0
- package/dist/migrate/types.js +8 -0
- package/dist/native-loader.d.ts +22 -0
- package/dist/native-loader.js +73 -0
- package/dist/parse.d.ts +36 -0
- package/dist/parse.js +95 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.js +2 -0
- package/dist/wasm-loader.d.ts +31 -0
- package/dist/wasm-loader.js +100 -0
- package/package.json +33 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic markdown section parser for CANT migration.
|
|
3
|
+
*
|
|
4
|
+
* Splits markdown content into sections by headings and classifies
|
|
5
|
+
* each section by matching against known agent/hook/skill/permission patterns.
|
|
6
|
+
* This is intentionally conservative -- unknown patterns are left as-is.
|
|
7
|
+
*/
|
|
8
|
+
import type { ExtractedPermission, ExtractedProperty, MarkdownSection, SectionClassification } from './types';
|
|
9
|
+
/**
|
|
10
|
+
* Parse markdown content into classified sections.
|
|
11
|
+
*
|
|
12
|
+
* Splits on `##` and `###` headings, classifies each section by
|
|
13
|
+
* heuristic pattern matching, and returns structured section data
|
|
14
|
+
* with line numbers for source mapping.
|
|
15
|
+
*
|
|
16
|
+
* @param content - Raw markdown content
|
|
17
|
+
* @returns Array of parsed and classified sections
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseMarkdownSections(content: string): MarkdownSection[];
|
|
20
|
+
/**
|
|
21
|
+
* Classify a markdown section based on its heading and content.
|
|
22
|
+
*
|
|
23
|
+
* Uses heuristic matching against known patterns. Returns 'unknown'
|
|
24
|
+
* for sections that cannot be confidently classified.
|
|
25
|
+
*
|
|
26
|
+
* @param section - The section to classify
|
|
27
|
+
* @returns The classification type
|
|
28
|
+
*/
|
|
29
|
+
export declare function classifySection(section: MarkdownSection): SectionClassification;
|
|
30
|
+
/**
|
|
31
|
+
* Extract key-value properties from markdown bullet lists.
|
|
32
|
+
*
|
|
33
|
+
* Matches patterns like:
|
|
34
|
+
* - `- **Key**: value`
|
|
35
|
+
* - `- **Key**: value`
|
|
36
|
+
* - `- Key: value`
|
|
37
|
+
*
|
|
38
|
+
* @param lines - Body lines of a section
|
|
39
|
+
* @returns Array of extracted properties
|
|
40
|
+
*/
|
|
41
|
+
export declare function extractProperties(lines: string[]): ExtractedProperty[];
|
|
42
|
+
/**
|
|
43
|
+
* Extract permission entries from markdown content.
|
|
44
|
+
*
|
|
45
|
+
* Handles two formats:
|
|
46
|
+
* 1. Structured: `- Tasks: read, write`
|
|
47
|
+
* 2. Prose: `- Read and write tasks`
|
|
48
|
+
*
|
|
49
|
+
* @param lines - Body lines of a permissions section
|
|
50
|
+
* @returns Array of extracted permissions
|
|
51
|
+
*/
|
|
52
|
+
export declare function extractPermissions(lines: string[]): ExtractedPermission[];
|
|
53
|
+
/**
|
|
54
|
+
* Normalize a heading into a valid CANT identifier.
|
|
55
|
+
*
|
|
56
|
+
* Converts "Code Review Agent" to "code-review-agent", strips
|
|
57
|
+
* common suffixes like " Agent", lowercases, and replaces
|
|
58
|
+
* non-alphanumeric chars with hyphens.
|
|
59
|
+
*
|
|
60
|
+
* @param heading - The raw heading text
|
|
61
|
+
* @returns A valid CANT identifier
|
|
62
|
+
*/
|
|
63
|
+
export declare function headingToIdentifier(heading: string): string;
|
|
64
|
+
/**
|
|
65
|
+
* Map a hook heading to a CAAMP event name.
|
|
66
|
+
*
|
|
67
|
+
* Converts headings like "On Session Start" to "SessionStart".
|
|
68
|
+
* Returns null if the heading does not match a known event.
|
|
69
|
+
*
|
|
70
|
+
* @param heading - The raw heading text
|
|
71
|
+
* @returns The CAAMP PascalCase event name, or null
|
|
72
|
+
*/
|
|
73
|
+
export declare function headingToEventName(heading: string): string | null;
|
|
74
|
+
/**
|
|
75
|
+
* Get the full set of CAAMP event names.
|
|
76
|
+
*
|
|
77
|
+
* @returns A read-only set of the 16 canonical CAAMP events
|
|
78
|
+
*/
|
|
79
|
+
export declare function getCaampEvents(): ReadonlySet<string>;
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Heuristic markdown section parser for CANT migration.
|
|
4
|
+
*
|
|
5
|
+
* Splits markdown content into sections by headings and classifies
|
|
6
|
+
* each section by matching against known agent/hook/skill/permission patterns.
|
|
7
|
+
* This is intentionally conservative -- unknown patterns are left as-is.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.parseMarkdownSections = parseMarkdownSections;
|
|
11
|
+
exports.classifySection = classifySection;
|
|
12
|
+
exports.extractProperties = extractProperties;
|
|
13
|
+
exports.extractPermissions = extractPermissions;
|
|
14
|
+
exports.headingToIdentifier = headingToIdentifier;
|
|
15
|
+
exports.headingToEventName = headingToEventName;
|
|
16
|
+
exports.getCaampEvents = getCaampEvents;
|
|
17
|
+
/**
|
|
18
|
+
* Known CAAMP hook event names (PascalCase).
|
|
19
|
+
* Used to identify hook sections from headings like "On Session Start".
|
|
20
|
+
*/
|
|
21
|
+
const CAAMP_EVENTS = new Set([
|
|
22
|
+
'SessionStart',
|
|
23
|
+
'SessionEnd',
|
|
24
|
+
'PromptSubmit',
|
|
25
|
+
'ResponseComplete',
|
|
26
|
+
'PreToolUse',
|
|
27
|
+
'PostToolUse',
|
|
28
|
+
'PostToolUseFailure',
|
|
29
|
+
'PermissionRequest',
|
|
30
|
+
'SubagentStart',
|
|
31
|
+
'SubagentStop',
|
|
32
|
+
'PreModel',
|
|
33
|
+
'PostModel',
|
|
34
|
+
'PreCompact',
|
|
35
|
+
'PostCompact',
|
|
36
|
+
'Notification',
|
|
37
|
+
'ConfigChange',
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Heading patterns that suggest a hook definition section.
|
|
41
|
+
* Case-insensitive matching against heading text.
|
|
42
|
+
*/
|
|
43
|
+
const HOOK_HEADING_PATTERNS = [
|
|
44
|
+
/^on\s+session\s*start/i,
|
|
45
|
+
/^on\s+session\s*end/i,
|
|
46
|
+
/^on\s+prompt\s*submit/i,
|
|
47
|
+
/^on\s+response\s*complete/i,
|
|
48
|
+
/^on\s+pre\s*tool\s*use/i,
|
|
49
|
+
/^on\s+post\s*tool\s*use/i,
|
|
50
|
+
/^on\s+post\s*tool\s*use\s*failure/i,
|
|
51
|
+
/^on\s+permission\s*request/i,
|
|
52
|
+
/^on\s+subagent\s*start/i,
|
|
53
|
+
/^on\s+subagent\s*stop/i,
|
|
54
|
+
/^on\s+pre\s*model/i,
|
|
55
|
+
/^on\s+post\s*model/i,
|
|
56
|
+
/^on\s+pre\s*compact/i,
|
|
57
|
+
/^on\s+post\s*compact/i,
|
|
58
|
+
/^on\s+notification/i,
|
|
59
|
+
/^on\s+config\s*change/i,
|
|
60
|
+
/^when\s+.*\s+start/i,
|
|
61
|
+
/^when\s+.*\s+end/i,
|
|
62
|
+
/^hooks?\b/i,
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* Parse markdown content into classified sections.
|
|
66
|
+
*
|
|
67
|
+
* Splits on `##` and `###` headings, classifies each section by
|
|
68
|
+
* heuristic pattern matching, and returns structured section data
|
|
69
|
+
* with line numbers for source mapping.
|
|
70
|
+
*
|
|
71
|
+
* @param content - Raw markdown content
|
|
72
|
+
* @returns Array of parsed and classified sections
|
|
73
|
+
*/
|
|
74
|
+
function parseMarkdownSections(content) {
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
const sections = [];
|
|
77
|
+
let currentSection = null;
|
|
78
|
+
for (let i = 0; i < lines.length; i++) {
|
|
79
|
+
const line = lines[i] ?? '';
|
|
80
|
+
const headingMatch = line.match(/^(#{2,3})\s+(.+)$/);
|
|
81
|
+
if (headingMatch) {
|
|
82
|
+
// Finalize previous section
|
|
83
|
+
if (currentSection?.heading) {
|
|
84
|
+
finalizeSection(currentSection, i, sections);
|
|
85
|
+
}
|
|
86
|
+
const level = (headingMatch[1] ?? '').length;
|
|
87
|
+
const heading = (headingMatch[2] ?? '').trim();
|
|
88
|
+
currentSection = {
|
|
89
|
+
heading,
|
|
90
|
+
level,
|
|
91
|
+
lineStart: i + 1, // 1-based
|
|
92
|
+
bodyLines: [],
|
|
93
|
+
classification: 'unknown',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
else if (currentSection) {
|
|
97
|
+
currentSection.bodyLines = currentSection.bodyLines ?? [];
|
|
98
|
+
currentSection.bodyLines.push(line);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Finalize last section
|
|
102
|
+
if (currentSection?.heading) {
|
|
103
|
+
finalizeSection(currentSection, lines.length, sections);
|
|
104
|
+
}
|
|
105
|
+
return sections;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Finalize a section: set lineEnd, trim trailing blank lines, classify.
|
|
109
|
+
*/
|
|
110
|
+
function finalizeSection(section, nextLineIndex, sections) {
|
|
111
|
+
section.lineEnd = nextLineIndex; // 1-based exclusive becomes the end
|
|
112
|
+
// Trim trailing blank lines from body
|
|
113
|
+
while (section.bodyLines.length > 0 &&
|
|
114
|
+
(section.bodyLines[section.bodyLines.length - 1] ?? '').trim() === '') {
|
|
115
|
+
section.bodyLines.pop();
|
|
116
|
+
}
|
|
117
|
+
section.classification = classifySection(section);
|
|
118
|
+
sections.push(section);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Classify a markdown section based on its heading and content.
|
|
122
|
+
*
|
|
123
|
+
* Uses heuristic matching against known patterns. Returns 'unknown'
|
|
124
|
+
* for sections that cannot be confidently classified.
|
|
125
|
+
*
|
|
126
|
+
* @param section - The section to classify
|
|
127
|
+
* @returns The classification type
|
|
128
|
+
*/
|
|
129
|
+
function classifySection(section) {
|
|
130
|
+
const heading = section.heading;
|
|
131
|
+
// Agent patterns: "## Agent: X", "## X Agent", "## Code Review Agent"
|
|
132
|
+
if (/\bagent\b/i.test(heading)) {
|
|
133
|
+
return 'agent';
|
|
134
|
+
}
|
|
135
|
+
// Permission patterns: "## Permissions", "### Permissions"
|
|
136
|
+
if (/^permissions?\b/i.test(heading)) {
|
|
137
|
+
return 'permissions';
|
|
138
|
+
}
|
|
139
|
+
// Hook patterns
|
|
140
|
+
for (const pattern of HOOK_HEADING_PATTERNS) {
|
|
141
|
+
if (pattern.test(heading)) {
|
|
142
|
+
return 'hook';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Skill patterns: "## Skills", "## Skill: X"
|
|
146
|
+
if (/\bskills?\b/i.test(heading)) {
|
|
147
|
+
return 'skill';
|
|
148
|
+
}
|
|
149
|
+
// Workflow/procedure patterns
|
|
150
|
+
if (/\b(workflow|procedure|deploy|pipeline)\b/i.test(heading)) {
|
|
151
|
+
return 'workflow';
|
|
152
|
+
}
|
|
153
|
+
// Try content-based classification for agent-like sections
|
|
154
|
+
if (hasAgentProperties(section.bodyLines)) {
|
|
155
|
+
return 'agent';
|
|
156
|
+
}
|
|
157
|
+
return 'unknown';
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check if body lines contain agent-like property patterns.
|
|
161
|
+
*
|
|
162
|
+
* Looks for key-value bullet lists with keys like "Model", "Prompt",
|
|
163
|
+
* "Persistence", "Skills", etc.
|
|
164
|
+
*/
|
|
165
|
+
function hasAgentProperties(bodyLines) {
|
|
166
|
+
const agentKeys = /\b(model|prompt|persist(ence)?|skills?)\b/i;
|
|
167
|
+
let matchCount = 0;
|
|
168
|
+
for (const line of bodyLines) {
|
|
169
|
+
if (/^[-*]\s+\*?\*?/.test(line) && agentKeys.test(line)) {
|
|
170
|
+
matchCount++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Need at least 2 agent-like properties to classify
|
|
174
|
+
return matchCount >= 2;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Extract key-value properties from markdown bullet lists.
|
|
178
|
+
*
|
|
179
|
+
* Matches patterns like:
|
|
180
|
+
* - `- **Key**: value`
|
|
181
|
+
* - `- **Key**: value`
|
|
182
|
+
* - `- Key: value`
|
|
183
|
+
*
|
|
184
|
+
* @param lines - Body lines of a section
|
|
185
|
+
* @returns Array of extracted properties
|
|
186
|
+
*/
|
|
187
|
+
function extractProperties(lines) {
|
|
188
|
+
const properties = [];
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
// Match: - **Key**: value or - Key: value
|
|
191
|
+
const match = line.match(/^[-*]\s+\*{0,2}([A-Za-z][A-Za-z0-9 _-]*?)\*{0,2}\s*:\s*(.+)$/);
|
|
192
|
+
if (match) {
|
|
193
|
+
const key = (match[1] ?? '').trim().toLowerCase();
|
|
194
|
+
const value = (match[2] ?? '').trim();
|
|
195
|
+
properties.push({ key, value });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return properties;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Extract permission entries from markdown content.
|
|
202
|
+
*
|
|
203
|
+
* Handles two formats:
|
|
204
|
+
* 1. Structured: `- Tasks: read, write`
|
|
205
|
+
* 2. Prose: `- Read and write tasks`
|
|
206
|
+
*
|
|
207
|
+
* @param lines - Body lines of a permissions section
|
|
208
|
+
* @returns Array of extracted permissions
|
|
209
|
+
*/
|
|
210
|
+
function extractPermissions(lines) {
|
|
211
|
+
const permissions = [];
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
// Format 1: "- Tasks: read, write"
|
|
214
|
+
const structuredMatch = line.match(/^[-*]\s+([A-Za-z]+)\s*:\s*(.+)$/);
|
|
215
|
+
if (structuredMatch) {
|
|
216
|
+
const domain = (structuredMatch[1] ?? '').trim().toLowerCase();
|
|
217
|
+
const rawValues = (structuredMatch[2] ?? '').trim();
|
|
218
|
+
const values = rawValues
|
|
219
|
+
.split(/[,\s]+/)
|
|
220
|
+
.map((v) => v.trim().toLowerCase())
|
|
221
|
+
.filter((v) => ['read', 'write', 'execute'].includes(v));
|
|
222
|
+
if (values.length > 0) {
|
|
223
|
+
permissions.push({ domain, values });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Format 2: "- Read and write tasks"
|
|
228
|
+
const proseMatch = line.match(/^[-*]\s+(read|write|execute)(?:\s+and\s+(read|write|execute))?\s+(\w+)/i);
|
|
229
|
+
if (proseMatch) {
|
|
230
|
+
const values = [(proseMatch[1] ?? '').toLowerCase()];
|
|
231
|
+
if (proseMatch[2]) {
|
|
232
|
+
values.push(proseMatch[2].toLowerCase());
|
|
233
|
+
}
|
|
234
|
+
const domain = (proseMatch[3] ?? '').toLowerCase();
|
|
235
|
+
permissions.push({ domain, values });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return permissions;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Normalize a heading into a valid CANT identifier.
|
|
242
|
+
*
|
|
243
|
+
* Converts "Code Review Agent" to "code-review-agent", strips
|
|
244
|
+
* common suffixes like " Agent", lowercases, and replaces
|
|
245
|
+
* non-alphanumeric chars with hyphens.
|
|
246
|
+
*
|
|
247
|
+
* @param heading - The raw heading text
|
|
248
|
+
* @returns A valid CANT identifier
|
|
249
|
+
*/
|
|
250
|
+
function headingToIdentifier(heading) {
|
|
251
|
+
return heading
|
|
252
|
+
.replace(/\bagent\b/gi, '')
|
|
253
|
+
.replace(/^[:]\s*/, '')
|
|
254
|
+
.trim()
|
|
255
|
+
.toLowerCase()
|
|
256
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
257
|
+
.replace(/^-+|-+$/g, '');
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Map a hook heading to a CAAMP event name.
|
|
261
|
+
*
|
|
262
|
+
* Converts headings like "On Session Start" to "SessionStart".
|
|
263
|
+
* Returns null if the heading does not match a known event.
|
|
264
|
+
*
|
|
265
|
+
* @param heading - The raw heading text
|
|
266
|
+
* @returns The CAAMP PascalCase event name, or null
|
|
267
|
+
*/
|
|
268
|
+
function headingToEventName(heading) {
|
|
269
|
+
// Direct "On EventName" pattern
|
|
270
|
+
const onMatch = heading.match(/^on\s+(.+)$/i);
|
|
271
|
+
if (onMatch) {
|
|
272
|
+
const eventCandidate = (onMatch[1] ?? '')
|
|
273
|
+
.trim()
|
|
274
|
+
.replace(/\s+/g, '')
|
|
275
|
+
.replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
276
|
+
if (CAAMP_EVENTS.has(eventCandidate)) {
|
|
277
|
+
return eventCandidate;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// "When session starts" -> "SessionStart" (common prose variant)
|
|
281
|
+
const whenMatch = heading.match(/^when\s+(?:a\s+)?(\w+)\s+starts?$/i);
|
|
282
|
+
if (whenMatch) {
|
|
283
|
+
const noun = (whenMatch[1] ?? '').trim();
|
|
284
|
+
const candidate = `${noun.charAt(0).toUpperCase()}${noun.slice(1).toLowerCase()}Start`;
|
|
285
|
+
if (CAAMP_EVENTS.has(candidate)) {
|
|
286
|
+
return candidate;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const whenEndMatch = heading.match(/^when\s+(?:a\s+)?(\w+)\s+ends?$/i);
|
|
290
|
+
if (whenEndMatch) {
|
|
291
|
+
const noun = (whenEndMatch[1] ?? '').trim();
|
|
292
|
+
const candidate = `${noun.charAt(0).toUpperCase()}${noun.slice(1).toLowerCase()}End`;
|
|
293
|
+
if (CAAMP_EVENTS.has(candidate)) {
|
|
294
|
+
return candidate;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Generic "Hooks" heading -> null (contains multiple hooks, not a single event)
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get the full set of CAAMP event names.
|
|
302
|
+
*
|
|
303
|
+
* @returns A read-only set of the 16 canonical CAAMP events
|
|
304
|
+
*/
|
|
305
|
+
function getCaampEvents() {
|
|
306
|
+
return CAAMP_EVENTS;
|
|
307
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CANT AST serializer -- generates .cant file text from structured data.
|
|
3
|
+
*
|
|
4
|
+
* Produces well-formatted .cant files with:
|
|
5
|
+
* - YAML frontmatter (kind, version)
|
|
6
|
+
* - 2-space indentation
|
|
7
|
+
* - Blank lines between top-level blocks
|
|
8
|
+
* - Proper quoting of string values
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtractedPermission, ExtractedProperty } from './types';
|
|
11
|
+
/**
|
|
12
|
+
* Intermediate representation of a CANT document for serialization.
|
|
13
|
+
*
|
|
14
|
+
* This is the bridge between the converter's output and the
|
|
15
|
+
* final .cant file text. Each field maps to a CANT construct.
|
|
16
|
+
*/
|
|
17
|
+
export interface CantDocumentIR {
|
|
18
|
+
/** Document kind (agent, skill, hook, workflow). */
|
|
19
|
+
kind: string;
|
|
20
|
+
/** Document version (default: 1). */
|
|
21
|
+
version: number;
|
|
22
|
+
/** The primary block (agent definition, hook, workflow, etc.). */
|
|
23
|
+
block: CantBlockIR;
|
|
24
|
+
}
|
|
25
|
+
/** A CANT block (agent, skill, hook, or workflow). */
|
|
26
|
+
export interface CantBlockIR {
|
|
27
|
+
/** Block type keyword (agent, skill, on, workflow, pipeline). */
|
|
28
|
+
type: string;
|
|
29
|
+
/** Block name/identifier (agent name, event name, workflow name). */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Key-value properties. */
|
|
32
|
+
properties: CantPropertyIR[];
|
|
33
|
+
/** Permission entries (only for agent blocks). */
|
|
34
|
+
permissions: ExtractedPermission[];
|
|
35
|
+
/** Nested sub-blocks (hooks inside agents, steps in pipelines, etc.). */
|
|
36
|
+
children: CantBlockIR[];
|
|
37
|
+
/** Raw body lines for hook/workflow bodies that are directive sequences. */
|
|
38
|
+
bodyLines?: string[];
|
|
39
|
+
}
|
|
40
|
+
/** A single property key-value pair. */
|
|
41
|
+
export interface CantPropertyIR {
|
|
42
|
+
/** Property key. */
|
|
43
|
+
key: string;
|
|
44
|
+
/** Property value (string, array, number, boolean). */
|
|
45
|
+
value: string | string[] | number | boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Serialize a CANT document IR into .cant file text.
|
|
49
|
+
*
|
|
50
|
+
* Produces a complete .cant file including frontmatter and body.
|
|
51
|
+
* All string values are quoted, arrays use bracket notation,
|
|
52
|
+
* and indentation uses 2 spaces per level.
|
|
53
|
+
*
|
|
54
|
+
* @param doc - The document IR to serialize
|
|
55
|
+
* @returns The complete .cant file content as a string
|
|
56
|
+
*/
|
|
57
|
+
export declare function serializeCantDocument(doc: CantDocumentIR): string;
|
|
58
|
+
/**
|
|
59
|
+
* Format a property value for .cant output.
|
|
60
|
+
*
|
|
61
|
+
* - Strings are double-quoted
|
|
62
|
+
* - Arrays use bracket notation with quoted elements
|
|
63
|
+
* - Numbers and booleans are bare
|
|
64
|
+
*
|
|
65
|
+
* @param value - The value to format
|
|
66
|
+
* @returns Formatted string
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatValue(value: string | string[] | number | boolean): string;
|
|
69
|
+
/**
|
|
70
|
+
* Convert extracted properties to CANT property IR format.
|
|
71
|
+
*
|
|
72
|
+
* Maps known markdown property keys to their CANT equivalents:
|
|
73
|
+
* - "model" -> model
|
|
74
|
+
* - "persistence" / "persist" -> persist
|
|
75
|
+
* - "prompt" -> prompt
|
|
76
|
+
* - "skills" -> skills (as array)
|
|
77
|
+
*
|
|
78
|
+
* @param properties - Extracted markdown properties
|
|
79
|
+
* @returns CANT property IR array
|
|
80
|
+
*/
|
|
81
|
+
export declare function propertiesToIR(properties: ExtractedProperty[]): CantPropertyIR[];
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CANT AST serializer -- generates .cant file text from structured data.
|
|
4
|
+
*
|
|
5
|
+
* Produces well-formatted .cant files with:
|
|
6
|
+
* - YAML frontmatter (kind, version)
|
|
7
|
+
* - 2-space indentation
|
|
8
|
+
* - Blank lines between top-level blocks
|
|
9
|
+
* - Proper quoting of string values
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.serializeCantDocument = serializeCantDocument;
|
|
13
|
+
exports.formatValue = formatValue;
|
|
14
|
+
exports.propertiesToIR = propertiesToIR;
|
|
15
|
+
/**
|
|
16
|
+
* Serialize a CANT document IR into .cant file text.
|
|
17
|
+
*
|
|
18
|
+
* Produces a complete .cant file including frontmatter and body.
|
|
19
|
+
* All string values are quoted, arrays use bracket notation,
|
|
20
|
+
* and indentation uses 2 spaces per level.
|
|
21
|
+
*
|
|
22
|
+
* @param doc - The document IR to serialize
|
|
23
|
+
* @returns The complete .cant file content as a string
|
|
24
|
+
*/
|
|
25
|
+
function serializeCantDocument(doc) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
// Frontmatter
|
|
28
|
+
lines.push('---');
|
|
29
|
+
lines.push(`kind: ${doc.kind}`);
|
|
30
|
+
lines.push(`version: ${doc.version}`);
|
|
31
|
+
lines.push('---');
|
|
32
|
+
lines.push('');
|
|
33
|
+
// Main block
|
|
34
|
+
serializeBlock(doc.block, 0, lines);
|
|
35
|
+
// Ensure trailing newline
|
|
36
|
+
return lines.join('\n') + '\n';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Serialize a single block at the given indentation level.
|
|
40
|
+
*/
|
|
41
|
+
function serializeBlock(block, indent, lines) {
|
|
42
|
+
const prefix = ' '.repeat(indent);
|
|
43
|
+
// Block header
|
|
44
|
+
lines.push(`${prefix}${block.type} ${block.name}:`);
|
|
45
|
+
const childIndent = indent + 1;
|
|
46
|
+
const childPrefix = ' '.repeat(childIndent);
|
|
47
|
+
// Properties
|
|
48
|
+
for (const prop of block.properties) {
|
|
49
|
+
lines.push(`${childPrefix}${prop.key}: ${formatValue(prop.value)}`);
|
|
50
|
+
}
|
|
51
|
+
// Permissions
|
|
52
|
+
if (block.permissions.length > 0) {
|
|
53
|
+
lines.push(`${childPrefix}permissions:`);
|
|
54
|
+
const permPrefix = ' '.repeat(childIndent + 1);
|
|
55
|
+
for (const perm of block.permissions) {
|
|
56
|
+
lines.push(`${permPrefix}${perm.domain}: ${perm.values.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Body lines (directives in hooks, etc.)
|
|
60
|
+
if (block.bodyLines && block.bodyLines.length > 0) {
|
|
61
|
+
for (const bodyLine of block.bodyLines) {
|
|
62
|
+
if (bodyLine.trim() === '') {
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
lines.push(`${childPrefix}${bodyLine}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Child blocks (nested hooks, pipeline steps, etc.)
|
|
71
|
+
if (block.children.length > 0) {
|
|
72
|
+
lines.push('');
|
|
73
|
+
for (const child of block.children) {
|
|
74
|
+
serializeBlock(child, childIndent, lines);
|
|
75
|
+
lines.push('');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Format a property value for .cant output.
|
|
81
|
+
*
|
|
82
|
+
* - Strings are double-quoted
|
|
83
|
+
* - Arrays use bracket notation with quoted elements
|
|
84
|
+
* - Numbers and booleans are bare
|
|
85
|
+
*
|
|
86
|
+
* @param value - The value to format
|
|
87
|
+
* @returns Formatted string
|
|
88
|
+
*/
|
|
89
|
+
function formatValue(value) {
|
|
90
|
+
if (typeof value === 'string') {
|
|
91
|
+
// Don't double-quote if already quoted or is a bare keyword
|
|
92
|
+
if (/^\d+$/.test(value) || value === 'true' || value === 'false') {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(value)) {
|
|
98
|
+
const elements = value.map((v) => `"${v.replace(/"/g, '\\"')}"`);
|
|
99
|
+
return `[${elements.join(', ')}]`;
|
|
100
|
+
}
|
|
101
|
+
// number or boolean
|
|
102
|
+
return String(value);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Convert extracted properties to CANT property IR format.
|
|
106
|
+
*
|
|
107
|
+
* Maps known markdown property keys to their CANT equivalents:
|
|
108
|
+
* - "model" -> model
|
|
109
|
+
* - "persistence" / "persist" -> persist
|
|
110
|
+
* - "prompt" -> prompt
|
|
111
|
+
* - "skills" -> skills (as array)
|
|
112
|
+
*
|
|
113
|
+
* @param properties - Extracted markdown properties
|
|
114
|
+
* @returns CANT property IR array
|
|
115
|
+
*/
|
|
116
|
+
function propertiesToIR(properties) {
|
|
117
|
+
const result = [];
|
|
118
|
+
for (const prop of properties) {
|
|
119
|
+
const key = normalizePropertyKey(prop.key);
|
|
120
|
+
const value = normalizePropertyValue(key, prop.value);
|
|
121
|
+
result.push({ key, value });
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Normalize a markdown property key to CANT form.
|
|
127
|
+
*/
|
|
128
|
+
function normalizePropertyKey(key) {
|
|
129
|
+
const keyMap = {
|
|
130
|
+
model: 'model',
|
|
131
|
+
persistence: 'persist',
|
|
132
|
+
persist: 'persist',
|
|
133
|
+
prompt: 'prompt',
|
|
134
|
+
skills: 'skills',
|
|
135
|
+
skill: 'skills',
|
|
136
|
+
description: 'description',
|
|
137
|
+
tier: 'tier',
|
|
138
|
+
};
|
|
139
|
+
return keyMap[key] ?? key;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Normalize a property value based on the key type.
|
|
143
|
+
*
|
|
144
|
+
* Skills are converted to arrays, others remain strings.
|
|
145
|
+
*/
|
|
146
|
+
function normalizePropertyValue(key, value) {
|
|
147
|
+
if (key === 'skills') {
|
|
148
|
+
// "ct-cleo, ct-orchestrator" -> ["ct-cleo", "ct-orchestrator"]
|
|
149
|
+
return value
|
|
150
|
+
.split(/[,\s]+/)
|
|
151
|
+
.map((v) => v.trim())
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|