@cleocode/cant 2026.4.11 → 2026.4.13

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.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Compiled bundle API for `.cant` files.
3
+ *
4
+ * @remarks
5
+ * Provides {@link compileBundle} which takes a list of `.cant` file paths,
6
+ * parses and validates each one via the existing cant-napi bridge, then
7
+ * collects the results into a single {@link CompiledBundle}. The bundle
8
+ * exposes extracted agents, teams, tools, and diagnostics, plus a
9
+ * {@link CompiledBundle.renderSystemPrompt | renderSystemPrompt()} method
10
+ * that produces a markdown-formatted system prompt addendum suitable for
11
+ * appending to a Pi system prompt.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * import { compileBundle } from '@cleocode/cant';
16
+ *
17
+ * const bundle = await compileBundle(['.cleo/cant/my-agent.cant']);
18
+ * if (bundle.valid) {
19
+ * const prompt = bundle.renderSystemPrompt();
20
+ * console.log(prompt);
21
+ * }
22
+ * ```
23
+ */
24
+ import type { CantDocumentResult } from './document.js';
25
+ /**
26
+ * A single parsed `.cant` document with its source path and diagnostics.
27
+ *
28
+ * @remarks
29
+ * The `document` field holds the raw AST as returned by
30
+ * {@link parseDocument}. Callers that need typed access should narrow the
31
+ * shape per the cant-core grammar (sections keyed by `Agent`, `Workflow`,
32
+ * `Pipeline`, etc.).
33
+ */
34
+ export interface ParsedCantDocument {
35
+ /** Absolute path to the source `.cant` file. */
36
+ sourcePath: string;
37
+ /** The document kind from frontmatter (`"Agent"`, `"Workflow"`, etc.), or `null`. */
38
+ kind: string | null;
39
+ /** The raw AST from `parseDocument`. `null` when parsing failed. */
40
+ document: CantDocumentResult['document'];
41
+ /** Validation diagnostics for this document. */
42
+ diagnostics: BundleDiagnostic[];
43
+ }
44
+ /** A normalized diagnostic combining parse errors and validation diagnostics. */
45
+ export interface BundleDiagnostic {
46
+ /** The rule ID (e.g., `"S01"`, `"parse"`). */
47
+ ruleId: string;
48
+ /** Human-readable diagnostic message. */
49
+ message: string;
50
+ /** Severity: `"error"`, `"warning"`, `"info"`, or `"hint"`. */
51
+ severity: string;
52
+ /** Source file path. */
53
+ sourcePath: string;
54
+ }
55
+ /**
56
+ * An agent declaration extracted from a compiled `.cant` file.
57
+ *
58
+ * @remarks
59
+ * Properties are stored as a flat `Record` with string keys. Values are
60
+ * simplified from the raw AST value wrapper (e.g., `{ Identifier: "worker" }`
61
+ * becomes `"worker"`).
62
+ */
63
+ export interface AgentEntry {
64
+ /** The agent name as declared in the `.cant` file. */
65
+ name: string;
66
+ /** Absolute path to the source `.cant` file. */
67
+ sourcePath: string;
68
+ /** Simplified agent properties (role, tier, prompt, skills, etc.). */
69
+ properties: Record<string, unknown>;
70
+ }
71
+ /**
72
+ * A team declaration extracted from a compiled `.cant` file.
73
+ *
74
+ * @remarks
75
+ * The current cant-core parser does not support `team` as a top-level
76
+ * section. This interface exists for forward-compatibility; the bundle
77
+ * will populate it once the grammar is extended.
78
+ */
79
+ export interface TeamEntry {
80
+ /** The team name as declared in the `.cant` file. */
81
+ name: string;
82
+ /** Absolute path to the source `.cant` file. */
83
+ sourcePath: string;
84
+ /** Simplified team properties. */
85
+ properties: Record<string, unknown>;
86
+ }
87
+ /**
88
+ * A tool declaration extracted from a compiled `.cant` file.
89
+ *
90
+ * @remarks
91
+ * The current cant-core parser does not support `tool` as a top-level
92
+ * section. This interface exists for forward-compatibility; the bundle
93
+ * will populate it once the grammar is extended.
94
+ */
95
+ export interface ToolEntry {
96
+ /** The tool name as declared in the `.cant` file. */
97
+ name: string;
98
+ /** Absolute path to the source `.cant` file. */
99
+ sourcePath: string;
100
+ /** Simplified tool properties. */
101
+ properties: Record<string, unknown>;
102
+ }
103
+ /**
104
+ * The result of compiling one or more `.cant` files into a unified bundle.
105
+ *
106
+ * @remarks
107
+ * Contains all successfully parsed documents, extracted entity entries
108
+ * (agents, teams, tools), cross-file diagnostics, and a
109
+ * {@link renderSystemPrompt} helper for Pi system prompt injection.
110
+ */
111
+ export interface CompiledBundle {
112
+ /** All successfully parsed documents, keyed by source path. */
113
+ documents: Map<string, ParsedCantDocument>;
114
+ /** Agents found across all documents. */
115
+ agents: AgentEntry[];
116
+ /** Teams found across all documents. */
117
+ teams: TeamEntry[];
118
+ /** Tools found across all documents. */
119
+ tools: ToolEntry[];
120
+ /** Validation diagnostics across all documents. */
121
+ diagnostics: BundleDiagnostic[];
122
+ /** Whether all documents parsed and validated without errors. */
123
+ valid: boolean;
124
+ /** Render the compiled bundle as a system prompt addendum. */
125
+ renderSystemPrompt(): string;
126
+ }
127
+ /**
128
+ * Compile a list of `.cant` files into a unified {@link CompiledBundle}.
129
+ *
130
+ * @remarks
131
+ * For each file path, reads the file, parses it via {@link parseDocument},
132
+ * validates it via {@link validateDocument}, then extracts agents, teams,
133
+ * and tools from the AST. Diagnostics from both parse errors and validation
134
+ * are collected into the bundle's {@link CompiledBundle.diagnostics} array.
135
+ *
136
+ * Files that fail to parse are still included in the bundle (with their
137
+ * diagnostics) but do not contribute entities. The bundle's `valid` flag
138
+ * is `true` only when every file parsed successfully and validated with
139
+ * zero error-severity diagnostics.
140
+ *
141
+ * @param filePaths - Absolute paths to `.cant` files to compile.
142
+ * @returns A {@link CompiledBundle} with all extracted entities and diagnostics.
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * import { compileBundle } from '@cleocode/cant';
147
+ *
148
+ * const bundle = await compileBundle([
149
+ * '/project/.cleo/cant/backend-dev.cant',
150
+ * '/project/.cleo/cant/frontend-dev.cant',
151
+ * ]);
152
+ *
153
+ * console.log(`Found ${bundle.agents.length} agents`);
154
+ * console.log(`Valid: ${bundle.valid}`);
155
+ * console.log(bundle.renderSystemPrompt());
156
+ * ```
157
+ */
158
+ export declare function compileBundle(filePaths: string[]): Promise<CompiledBundle>;
package/dist/bundle.js ADDED
@@ -0,0 +1,423 @@
1
+ "use strict";
2
+ /**
3
+ * Compiled bundle API for `.cant` files.
4
+ *
5
+ * @remarks
6
+ * Provides {@link compileBundle} which takes a list of `.cant` file paths,
7
+ * parses and validates each one via the existing cant-napi bridge, then
8
+ * collects the results into a single {@link CompiledBundle}. The bundle
9
+ * exposes extracted agents, teams, tools, and diagnostics, plus a
10
+ * {@link CompiledBundle.renderSystemPrompt | renderSystemPrompt()} method
11
+ * that produces a markdown-formatted system prompt addendum suitable for
12
+ * appending to a Pi system prompt.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { compileBundle } from '@cleocode/cant';
17
+ *
18
+ * const bundle = await compileBundle(['.cleo/cant/my-agent.cant']);
19
+ * if (bundle.valid) {
20
+ * const prompt = bundle.renderSystemPrompt();
21
+ * console.log(prompt);
22
+ * }
23
+ * ```
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.compileBundle = compileBundle;
27
+ const document_js_1 = require("./document.js");
28
+ /**
29
+ * Simplify a cant-core AST value wrapper to a plain JS value.
30
+ *
31
+ * @remarks
32
+ * The AST wraps values like `{ Identifier: "worker" }`,
33
+ * `{ String: { raw: "..." } }`, `{ Number: 2 }`, `{ Boolean: true }`,
34
+ * `{ Array: [...] }`, `{ ProseBlock: { lines: [...] } }`. This function
35
+ * extracts the inner payload for human-readable property maps.
36
+ */
37
+ function simplifyValue(wrapper) {
38
+ if ('Identifier' in wrapper)
39
+ return wrapper['Identifier'];
40
+ if ('String' in wrapper) {
41
+ const inner = wrapper['String'];
42
+ if (typeof inner === 'object' && inner !== null && 'raw' in inner) {
43
+ return inner.raw;
44
+ }
45
+ return inner;
46
+ }
47
+ if ('Number' in wrapper)
48
+ return wrapper['Number'];
49
+ if ('Boolean' in wrapper)
50
+ return wrapper['Boolean'];
51
+ if ('ProseBlock' in wrapper) {
52
+ const block = wrapper['ProseBlock'];
53
+ if (typeof block === 'object' && block !== null && 'lines' in block) {
54
+ return block.lines.join('\n');
55
+ }
56
+ return block;
57
+ }
58
+ if ('Array' in wrapper) {
59
+ const arr = wrapper['Array'];
60
+ if (Array.isArray(arr)) {
61
+ return arr.map((item) => simplifyValue(item));
62
+ }
63
+ return arr;
64
+ }
65
+ // Fallback: return the wrapper as-is for unknown shapes
66
+ return wrapper;
67
+ }
68
+ /**
69
+ * Extract a flat properties map from raw AST properties.
70
+ *
71
+ * @param rawProps - The raw AST property array from a section.
72
+ * @returns A simplified `Record<string, unknown>` map.
73
+ */
74
+ function extractProperties(rawProps) {
75
+ const result = {};
76
+ for (const prop of rawProps) {
77
+ const key = prop.key?.value;
78
+ if (typeof key !== 'string')
79
+ continue;
80
+ result[key] = simplifyValue(prop.value);
81
+ }
82
+ return result;
83
+ }
84
+ /**
85
+ * Extract agents from a parsed document AST.
86
+ *
87
+ * @param doc - The raw AST document object from `parseDocument`.
88
+ * @param sourcePath - The source file path for attribution.
89
+ * @returns An array of {@link AgentEntry} extracted from `Agent` sections.
90
+ */
91
+ function extractAgents(doc, sourcePath) {
92
+ if (typeof doc !== 'object' || doc === null)
93
+ return [];
94
+ const docObj = doc;
95
+ const sections = docObj['sections'];
96
+ if (!Array.isArray(sections))
97
+ return [];
98
+ const agents = [];
99
+ for (const section of sections) {
100
+ if (typeof section !== 'object' || section === null)
101
+ continue;
102
+ const wrapper = section;
103
+ const agentData = wrapper['Agent'];
104
+ if (!agentData)
105
+ continue;
106
+ const nameValue = agentData.name?.value;
107
+ if (typeof nameValue !== 'string')
108
+ continue;
109
+ const properties = extractProperties(agentData.properties ?? []);
110
+ // Include permissions as a property for visibility
111
+ if (Array.isArray(agentData.permissions) && agentData.permissions.length > 0) {
112
+ const permMap = {};
113
+ for (const perm of agentData.permissions) {
114
+ if (typeof perm.domain === 'string' && Array.isArray(perm.access)) {
115
+ permMap[perm.domain] = perm.access;
116
+ }
117
+ }
118
+ properties['permissions'] = permMap;
119
+ }
120
+ agents.push({ name: nameValue, sourcePath, properties });
121
+ }
122
+ return agents;
123
+ }
124
+ /**
125
+ * Extract teams from a parsed document AST.
126
+ *
127
+ * @remarks
128
+ * The current cant-core parser does not support `team` sections. This
129
+ * function is a forward-compatible stub that will extract teams once the
130
+ * grammar adds `team` as a recognized top-level construct.
131
+ *
132
+ * @param doc - The raw AST document object from `parseDocument`.
133
+ * @param sourcePath - The source file path for attribution.
134
+ * @returns An array of {@link TeamEntry} (currently always empty).
135
+ */
136
+ function extractTeams(doc, sourcePath) {
137
+ if (typeof doc !== 'object' || doc === null)
138
+ return [];
139
+ const docObj = doc;
140
+ const sections = docObj['sections'];
141
+ if (!Array.isArray(sections))
142
+ return [];
143
+ const teams = [];
144
+ for (const section of sections) {
145
+ if (typeof section !== 'object' || section === null)
146
+ continue;
147
+ const wrapper = section;
148
+ const teamData = wrapper['Team'];
149
+ if (!teamData)
150
+ continue;
151
+ const nameValue = teamData.name?.value;
152
+ if (typeof nameValue !== 'string')
153
+ continue;
154
+ teams.push({
155
+ name: nameValue,
156
+ sourcePath,
157
+ properties: extractProperties(teamData.properties ?? []),
158
+ });
159
+ }
160
+ return teams;
161
+ }
162
+ /**
163
+ * Extract tools from a parsed document AST.
164
+ *
165
+ * @remarks
166
+ * The current cant-core parser does not support `tool` sections. This
167
+ * function is a forward-compatible stub that will extract tools once the
168
+ * grammar adds `tool` as a recognized top-level construct.
169
+ *
170
+ * @param doc - The raw AST document object from `parseDocument`.
171
+ * @param sourcePath - The source file path for attribution.
172
+ * @returns An array of {@link ToolEntry} (currently always empty).
173
+ */
174
+ function extractTools(doc, sourcePath) {
175
+ if (typeof doc !== 'object' || doc === null)
176
+ return [];
177
+ const docObj = doc;
178
+ const sections = docObj['sections'];
179
+ if (!Array.isArray(sections))
180
+ return [];
181
+ const tools = [];
182
+ for (const section of sections) {
183
+ if (typeof section !== 'object' || section === null)
184
+ continue;
185
+ const wrapper = section;
186
+ const toolData = wrapper['Tool'];
187
+ if (!toolData)
188
+ continue;
189
+ const nameValue = toolData.name?.value;
190
+ if (typeof nameValue !== 'string')
191
+ continue;
192
+ tools.push({
193
+ name: nameValue,
194
+ sourcePath,
195
+ properties: extractProperties(toolData.properties ?? []),
196
+ });
197
+ }
198
+ return tools;
199
+ }
200
+ /**
201
+ * Render a system prompt addendum from the compiled bundle contents.
202
+ *
203
+ * @remarks
204
+ * Produces markdown suitable for appending to a Pi system prompt. Lists
205
+ * all declared agents with their roles, tiers, and descriptions; all
206
+ * teams with orchestrators and members; and all tools with descriptions.
207
+ *
208
+ * @param bundle - The compiled bundle to render.
209
+ * @returns A markdown-formatted string, or an empty string if the bundle is empty.
210
+ */
211
+ function renderBundleSystemPrompt(bundle) {
212
+ const lines = [];
213
+ if (bundle.agents.length === 0 && bundle.teams.length === 0 && bundle.tools.length === 0) {
214
+ return '';
215
+ }
216
+ lines.push('## CANT Bundle — Loaded Declarations');
217
+ lines.push('');
218
+ if (bundle.agents.length > 0) {
219
+ lines.push('### Agents');
220
+ lines.push('');
221
+ for (const agent of bundle.agents) {
222
+ const role = typeof agent.properties['role'] === 'string' ? agent.properties['role'] : 'unspecified';
223
+ const tier = typeof agent.properties['tier'] === 'string' ? agent.properties['tier'] : 'unspecified';
224
+ const prompt = typeof agent.properties['prompt'] === 'string' ? agent.properties['prompt'] : '';
225
+ const description = prompt.length > 0 ? prompt.split('\n')[0] : '';
226
+ lines.push(`- **${agent.name}** (role: ${role}, tier: ${tier})`);
227
+ if (description.length > 0) {
228
+ lines.push(` ${description.trim()}`);
229
+ }
230
+ }
231
+ lines.push('');
232
+ }
233
+ if (bundle.teams.length > 0) {
234
+ lines.push('### Teams');
235
+ lines.push('');
236
+ for (const team of bundle.teams) {
237
+ const orchestrator = typeof team.properties['orchestrator'] === 'string'
238
+ ? team.properties['orchestrator']
239
+ : 'unspecified';
240
+ const description = typeof team.properties['description'] === 'string'
241
+ ? team.properties['description']
242
+ : '';
243
+ lines.push(`- **${team.name}** (orchestrator: ${orchestrator})`);
244
+ if (description.length > 0) {
245
+ lines.push(` ${description.trim()}`);
246
+ }
247
+ }
248
+ lines.push('');
249
+ }
250
+ if (bundle.tools.length > 0) {
251
+ lines.push('### Tools');
252
+ lines.push('');
253
+ for (const tool of bundle.tools) {
254
+ const description = typeof tool.properties['description'] === 'string'
255
+ ? tool.properties['description']
256
+ : '';
257
+ lines.push(`- **${tool.name}**`);
258
+ if (description.length > 0) {
259
+ lines.push(` ${description.trim()}`);
260
+ }
261
+ }
262
+ lines.push('');
263
+ }
264
+ if (!bundle.valid && bundle.diagnostics.length > 0) {
265
+ const errorCount = bundle.diagnostics.filter(d => d.severity === 'error').length;
266
+ if (errorCount > 0) {
267
+ lines.push(`> **Warning**: ${errorCount} validation error(s) found across .cant files.`);
268
+ lines.push('');
269
+ }
270
+ }
271
+ return lines.join('\n');
272
+ }
273
+ // ---------------------------------------------------------------------------
274
+ // Public API
275
+ // ---------------------------------------------------------------------------
276
+ /**
277
+ * Compile a list of `.cant` files into a unified {@link CompiledBundle}.
278
+ *
279
+ * @remarks
280
+ * For each file path, reads the file, parses it via {@link parseDocument},
281
+ * validates it via {@link validateDocument}, then extracts agents, teams,
282
+ * and tools from the AST. Diagnostics from both parse errors and validation
283
+ * are collected into the bundle's {@link CompiledBundle.diagnostics} array.
284
+ *
285
+ * Files that fail to parse are still included in the bundle (with their
286
+ * diagnostics) but do not contribute entities. The bundle's `valid` flag
287
+ * is `true` only when every file parsed successfully and validated with
288
+ * zero error-severity diagnostics.
289
+ *
290
+ * @param filePaths - Absolute paths to `.cant` files to compile.
291
+ * @returns A {@link CompiledBundle} with all extracted entities and diagnostics.
292
+ *
293
+ * @example
294
+ * ```typescript
295
+ * import { compileBundle } from '@cleocode/cant';
296
+ *
297
+ * const bundle = await compileBundle([
298
+ * '/project/.cleo/cant/backend-dev.cant',
299
+ * '/project/.cleo/cant/frontend-dev.cant',
300
+ * ]);
301
+ *
302
+ * console.log(`Found ${bundle.agents.length} agents`);
303
+ * console.log(`Valid: ${bundle.valid}`);
304
+ * console.log(bundle.renderSystemPrompt());
305
+ * ```
306
+ */
307
+ async function compileBundle(filePaths) {
308
+ const documents = new Map();
309
+ const allAgents = [];
310
+ const allTeams = [];
311
+ const allTools = [];
312
+ const allDiagnostics = [];
313
+ let allValid = true;
314
+ for (const filePath of filePaths) {
315
+ const fileDiagnostics = [];
316
+ // Parse the document
317
+ let parseResult;
318
+ try {
319
+ parseResult = await (0, document_js_1.parseDocument)(filePath);
320
+ }
321
+ catch (err) {
322
+ const message = err instanceof Error ? err.message : String(err);
323
+ fileDiagnostics.push({
324
+ ruleId: 'parse',
325
+ message: `Failed to read or parse file: ${message}`,
326
+ severity: 'error',
327
+ sourcePath: filePath,
328
+ });
329
+ allDiagnostics.push(...fileDiagnostics);
330
+ documents.set(filePath, {
331
+ sourcePath: filePath,
332
+ kind: null,
333
+ document: null,
334
+ diagnostics: fileDiagnostics,
335
+ });
336
+ allValid = false;
337
+ continue;
338
+ }
339
+ // Convert parse errors to bundle diagnostics
340
+ if (!parseResult.success) {
341
+ for (const err of parseResult.errors) {
342
+ fileDiagnostics.push({
343
+ ruleId: 'parse',
344
+ message: err.message,
345
+ severity: err.severity,
346
+ sourcePath: filePath,
347
+ });
348
+ }
349
+ allValid = false;
350
+ }
351
+ // Extract document kind from AST
352
+ let kind = null;
353
+ if (parseResult.document !== null && typeof parseResult.document === 'object') {
354
+ const docObj = parseResult.document;
355
+ if (typeof docObj['kind'] === 'string') {
356
+ kind = docObj['kind'];
357
+ }
358
+ }
359
+ // Validate if parsing succeeded
360
+ if (parseResult.success) {
361
+ let validationResult;
362
+ try {
363
+ validationResult = await (0, document_js_1.validateDocument)(filePath);
364
+ }
365
+ catch (err) {
366
+ const message = err instanceof Error ? err.message : String(err);
367
+ fileDiagnostics.push({
368
+ ruleId: 'validate',
369
+ message: `Validation failed: ${message}`,
370
+ severity: 'error',
371
+ sourcePath: filePath,
372
+ });
373
+ allDiagnostics.push(...fileDiagnostics);
374
+ documents.set(filePath, {
375
+ sourcePath: filePath,
376
+ kind,
377
+ document: parseResult.document,
378
+ diagnostics: fileDiagnostics,
379
+ });
380
+ allValid = false;
381
+ continue;
382
+ }
383
+ // Convert validation diagnostics
384
+ for (const diag of validationResult.diagnostics) {
385
+ fileDiagnostics.push({
386
+ ruleId: diag.ruleId,
387
+ message: diag.message,
388
+ severity: diag.severity,
389
+ sourcePath: filePath,
390
+ });
391
+ }
392
+ if (!validationResult.valid) {
393
+ allValid = false;
394
+ }
395
+ // Extract entities from successfully parsed documents
396
+ const agents = extractAgents(parseResult.document, filePath);
397
+ const teams = extractTeams(parseResult.document, filePath);
398
+ const tools = extractTools(parseResult.document, filePath);
399
+ allAgents.push(...agents);
400
+ allTeams.push(...teams);
401
+ allTools.push(...tools);
402
+ }
403
+ allDiagnostics.push(...fileDiagnostics);
404
+ documents.set(filePath, {
405
+ sourcePath: filePath,
406
+ kind,
407
+ document: parseResult.document,
408
+ diagnostics: fileDiagnostics,
409
+ });
410
+ }
411
+ const bundle = {
412
+ documents,
413
+ agents: allAgents,
414
+ teams: allTeams,
415
+ tools: allTools,
416
+ diagnostics: allDiagnostics,
417
+ valid: allValid,
418
+ renderSystemPrompt() {
419
+ return renderBundleSystemPrompt(this);
420
+ },
421
+ };
422
+ return bundle;
423
+ }